├── .editorconfig ├── .github └── workflows │ └── build.yml ├── .gitignore ├── .gitmodules ├── .rustfmt.toml ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── Makefile ├── README.md ├── examples ├── example-fungibles │ ├── Cargo.toml │ └── src │ │ └── main.rs └── example-helloworld │ ├── Cargo.toml │ └── src │ └── main.rs ├── guest-examples ├── .cargo │ └── config.toml ├── Cargo.lock ├── Cargo.toml ├── rust-toolchain.toml ├── sum-balance-hand-written │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── sum-balance-percent │ ├── Cargo.toml │ ├── build.rs │ └── src │ │ └── main.rs ├── sum-balance │ ├── Cargo.toml │ ├── build.rs │ └── src │ │ └── main.rs ├── test-swap-extension │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── total-supply-hand-written │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── total-supply │ ├── Cargo.toml │ └── src │ │ └── main.rs └── transparent-call-hand-written │ ├── Cargo.toml │ └── src │ └── main.rs ├── poc └── runtime │ ├── Cargo.toml │ ├── build.rs │ ├── chopsticks.yml │ └── src │ ├── lib.rs │ └── pvq.rs ├── pvq-executor ├── Cargo.toml └── src │ ├── context.rs │ ├── error.rs │ ├── executor.rs │ └── lib.rs ├── pvq-extension-core ├── Cargo.toml └── src │ └── lib.rs ├── pvq-extension-fungibles ├── Cargo.toml └── src │ └── lib.rs ├── pvq-extension-swap ├── Cargo.toml └── src │ └── lib.rs ├── pvq-extension ├── Cargo.toml ├── procedural │ ├── Cargo.toml │ ├── examples │ │ └── proc_main │ │ │ ├── extension_decl.rs │ │ │ ├── extension_impl.rs │ │ │ └── main.rs │ ├── src │ │ ├── extension_decl │ │ │ ├── expand │ │ │ │ ├── extension.rs │ │ │ │ ├── helper.rs │ │ │ │ ├── metadata.rs │ │ │ │ └── mod.rs │ │ │ ├── mod.rs │ │ │ └── parse │ │ │ │ ├── extension.rs │ │ │ │ ├── helper.rs │ │ │ │ └── mod.rs │ │ ├── extensions_impl │ │ │ ├── expand │ │ │ │ ├── metadata.rs │ │ │ │ └── mod.rs │ │ │ ├── mod.rs │ │ │ └── parse │ │ │ │ ├── extension.rs │ │ │ │ ├── helper.rs │ │ │ │ ├── impl_struct.rs │ │ │ │ └── mod.rs │ │ ├── lib.rs │ │ └── utils.rs │ └── tests │ │ ├── tests.rs │ │ └── ui │ │ ├── extension_decl │ │ ├── not_on_mod.rs │ │ ├── not_on_mod.stderr │ │ └── pass │ │ │ └── sucess.rs │ │ └── extensions_impl │ │ ├── missing_impl_struct.rs │ │ ├── missing_impl_struct.stderr │ │ └── pass │ │ └── success.rs └── src │ ├── calldata.rs │ ├── context.rs │ ├── error.rs │ ├── executor.rs │ ├── lib.rs │ ├── macros.rs │ ├── metadata.rs │ └── perm_controller.rs ├── pvq-primitives ├── Cargo.toml └── src │ └── lib.rs ├── pvq-program-metadata-gen ├── Cargo.toml ├── README.md └── src │ ├── bin │ └── pvq-program-metadata-gen.rs │ ├── features.rs │ ├── helper.rs │ ├── lib.rs │ ├── manifest.rs │ └── metadata_gen.rs ├── pvq-program ├── Cargo.toml ├── procedural │ ├── Cargo.toml │ └── src │ │ ├── lib.rs │ │ └── program │ │ ├── expand │ │ ├── mod.rs │ │ └── preludes.rs │ │ ├── mod.rs │ │ └── parse │ │ ├── entrypoint.rs │ │ ├── extension_fn.rs │ │ ├── helper.rs │ │ └── mod.rs └── src │ └── lib.rs ├── pvq-runtime-api ├── Cargo.toml └── src │ └── lib.rs ├── pvq-test-runner ├── Cargo.toml └── src │ ├── bin │ └── pvq-test-runner.rs │ └── lib.rs └── rust-toolchain.toml /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | [*] 3 | indent_style=space 4 | indent_size=tab 5 | tab_width=4 6 | end_of_line=lf 7 | charset=utf-8 8 | trim_trailing_whitespace=true 9 | max_line_length=120 10 | insert_final_newline=true 11 | 12 | [Makefile] 13 | indent_style=tab 14 | 15 | [*.{yml, yaml}] 16 | indent_style = space 17 | indent_size = 2 18 | tab_width = 8 19 | end_of_line = lf 20 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: ["master"] 6 | pull_request: 7 | branches: ["master"] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | concurrency: 13 | group: ${{ github.workflow }}-${{ github.ref }} 14 | cancel-in-progress: true 15 | 16 | jobs: 17 | check: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v4 21 | with: 22 | submodules: recursive 23 | 24 | - name: Install toolchain 25 | uses: dtolnay/rust-toolchain@stable 26 | with: 27 | targets: wasm32-unknown-unknown 28 | components: rust-src, rustfmt, clippy 29 | 30 | - uses: Swatinem/rust-cache@v2 31 | 32 | - name: Check format 33 | run: cargo fmt --all -- --check 34 | 35 | - name: Make dummy poc-guest files 36 | run: make dummy-guests 37 | 38 | - name: Check no-std 39 | run: make check-wasm 40 | 41 | - name: Install pvq-program-metadata-gen 42 | run: cargo install --path pvq-program-metadata-gen 43 | 44 | - name: Clippy 45 | run: make clippy 46 | 47 | - name: Run tests 48 | run: make test 49 | 50 | build-guest: 51 | runs-on: ubuntu-latest 52 | steps: 53 | - uses: actions/checkout@v4 54 | with: 55 | submodules: recursive 56 | 57 | - name: Install toolchain 58 | uses: dtolnay/rust-toolchain@stable 59 | with: 60 | components: rust-src, rustfmt, clippy 61 | 62 | - uses: Swatinem/rust-cache@v2 63 | with: 64 | workspaces: | 65 | guest-examples -> guest-examples/target 66 | cache-all-crates: true 67 | 68 | - name: Install polkatool 69 | run: make polkatool 70 | 71 | - name: Install pvq-program-metadata-gen 72 | run: cargo install --path pvq-program-metadata-gen 73 | 74 | - name: Build guests 75 | run: make guests 76 | 77 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 7 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 8 | #Cargo.lock 9 | 10 | # These are backup files generated by rustfmt 11 | **/*.rs.bk 12 | 13 | # MSVC Windows builds of rustc generate these, which store debugging information 14 | *.pdb 15 | 16 | # MacOS related 17 | .DS_Store 18 | 19 | # VSCode related 20 | .vscode/ 21 | output/ 22 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "vendor/polkavm"] 2 | path = vendor/polkavm 3 | url = https://github.com/paritytech/polkavm 4 | [submodule "vendor/polkadot-sdk"] 5 | path = vendor/polkadot-sdk 6 | url = https://github.com/indirection42/polkadot-sdk 7 | branch = pvq-with-xcm-poc 8 | 9 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | reorder_imports = true 2 | max_width = 120 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace.package] 2 | authors = ["Acala Developers "] 3 | edition = "2021" 4 | repository = "https://github.com/open-web3-stack/PVQ" 5 | license = "Apache-2.0" 6 | version = "0.1.0" 7 | 8 | [workspace] 9 | resolver = "2" 10 | members = [ 11 | "poc/runtime", 12 | 13 | "pvq-program", 14 | "pvq-program-metadata-gen", 15 | "pvq-executor", 16 | "pvq-extension-core", 17 | "pvq-extension-fungibles", 18 | "pvq-extension-swap", 19 | "pvq-extension", 20 | "pvq-primitives", 21 | "pvq-runtime-api", 22 | "pvq-test-runner", 23 | 24 | "examples/example-fungibles", 25 | "examples/example-helloworld", 26 | ] 27 | exclude = ["guest-examples", "vendor"] 28 | 29 | [profile.release] 30 | # runtime requires unwinding. 31 | panic = "unwind" 32 | opt-level = 3 33 | 34 | [workspace.dependencies] 35 | # local 36 | pvq-program = { path = "pvq-program", default-features = false } 37 | pvq-program-metadata-gen = { path = "pvq-program-metadata-gen" } 38 | pvq-executor = { path = "pvq-executor", default-features = false } 39 | pvq-extension-core = { path = "pvq-extension-core", default-features = false } 40 | pvq-extension-fungibles = { path = "pvq-extension-fungibles", default-features = false } 41 | pvq-extension-swap = { path = "pvq-extension-swap", default-features = false } 42 | pvq-extension = { path = "pvq-extension", default-features = false } 43 | pvq-primitives = { path = "pvq-primitives", default-features = false } 44 | pvq-runtime-api = { path = "pvq-runtime-api", default-features = false } 45 | pvq-test-runner = { path = "pvq-test-runner", default-features = false } 46 | 47 | # polkavm 48 | polkavm = { path = "vendor/polkavm/crates/polkavm", default-features = false } 49 | polkavm-derive = { path = "vendor/polkavm/crates/polkavm-derive", default-features = false } 50 | 51 | # polkadot-sdk 52 | sp-api = { path = "vendor/polkadot-sdk/substrate/primitives/api", default-features = false } 53 | sp-core = { path = "vendor/polkadot-sdk/substrate/primitives/core", default-features = false } 54 | frame = { package = "polkadot-sdk-frame", path = "vendor/polkadot-sdk/substrate/frame", default-features = false } 55 | pallet-balances = { path = "vendor/polkadot-sdk/substrate/frame/balances", default-features = false } 56 | pallet-assets = { path = "vendor/polkadot-sdk/substrate/frame/assets", default-features = false } 57 | pallet-sudo = { path = "vendor/polkadot-sdk/substrate/frame/sudo", default-features = false } 58 | pallet-timestamp = { path = "vendor/polkadot-sdk/substrate/frame/timestamp", default-features = false } 59 | pallet-transaction-payment = { path = "vendor/polkadot-sdk/substrate/frame/transaction-payment", default-features = false } 60 | pallet-transaction-payment-rpc-runtime-api = { path = "vendor/polkadot-sdk/substrate/frame/transaction-payment/rpc/runtime-api", default-features = false } 61 | 62 | # genesis builder that allows us to interacto with runtime genesis config 63 | sp-genesis-builder = { path = "vendor/polkadot-sdk/substrate/primitives/genesis-builder", default-features = false } 64 | 65 | # wasm builder 66 | substrate-wasm-builder = { version = "22.0.1" } 67 | 68 | # nostd 69 | parity-scale-codec = { version = "3.6.12", default-features = false, features = [ 70 | "derive", 71 | "max-encoded-len", 72 | ] } 73 | scale-info = { version = "2.11.3", default-features = false, features = [ 74 | "derive", 75 | "serde", 76 | ] } 77 | tracing = { version = "0.1.40", default-features = false } 78 | serde = { version = "1.0.215", default-features = false, features = ["derive"] } 79 | serde_json = { version = "1.0.110", default-features = false } 80 | 81 | # std 82 | clap = { version = "4.5.4", features = ["derive"] } 83 | env_logger = { version = "0.11.3" } 84 | tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } 85 | tempfile = { version = "3.9.0" } 86 | toml = { version = "0.8", features = ["preserve_order"] } 87 | 88 | fortuples = "0.9" 89 | 90 | # proc macros 91 | syn = { version = "2", features = ["full", "visit-mut", "extra-traits"] } 92 | quote = "1" 93 | proc-macro2 = "1" 94 | proc-macro-crate = "3" 95 | trybuild = "1" 96 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | run: chainspec 2 | bunx @acala-network/chopsticks@latest --config poc/runtime/chopsticks.yml --genesis output/chainspec.json 3 | 4 | GUEST_EXAMPLES = $(shell find guest-examples -name "Cargo.toml" -not -path "guest-examples/Cargo.toml" | xargs -n1 dirname | xargs -n1 basename) 5 | GUEST_TARGETS = $(patsubst %,guest-%,$(GUEST_EXAMPLES)) 6 | DUMMY_GUEST_TARGETS = $(patsubst %,dummy-guest-%,$(GUEST_EXAMPLES)) 7 | 8 | guests: $(GUEST_TARGETS) 9 | 10 | dummy-guests: $(DUMMY_GUEST_TARGETS) 11 | 12 | guest-%: 13 | cd guest-examples; METADATA_OUTPUT_DIR=$(realpath output) cargo build -q --release --bin guest-$* -p guest-$* 14 | mkdir -p output 15 | polkatool link --run-only-if-newer -s guest-examples/target/riscv32emac-unknown-none-polkavm/release/guest-$* -o output/guest-$*.polkavm 16 | 17 | dummy-guest-%: 18 | mkdir -p output 19 | touch output/guest-$*.polkavm 20 | 21 | tools: polkatool chain-spec-builder 22 | 23 | polkatool: 24 | cargo install --path vendor/polkavm/tools/polkatool 25 | 26 | pvq-program-metadata-gen: 27 | cargo install --path pvq-program-metadata-gen 28 | 29 | chain-spec-builder: 30 | cargo install --path vendor/polkadot-sdk/substrate/bin/utils/chain-spec-builder 31 | 32 | fmt: 33 | cargo fmt --all 34 | 35 | check-wasm: 36 | cargo check --no-default-features --target=wasm32-unknown-unknown -p pvq-program -p pvq-executor -p pvq-extension-core -p pvq-extension-fungibles -p pvq-extension -p pvq-primitives -p pvq-runtime-api 37 | SKIP_WASM_BUILD= cargo check --no-default-features --target=wasm32-unknown-unknown -p poc-runtime 38 | 39 | check: check-wasm 40 | SKIP_WASM_BUILD= cargo check 41 | cd pvq-program/examples; cargo check 42 | 43 | clippy: 44 | SKIP_WASM_BUILD= cargo clippy -- -D warnings 45 | cd guest-examples; METADATA_OUTPUT_DIR=$(realpath output) cargo clippy --all 46 | 47 | test: 48 | SKIP_WASM_BUILD= cargo test 49 | 50 | chainspec: 51 | cargo build -p poc-runtime --release 52 | mkdir -p output 53 | cp target/release/wbuild/poc-runtime/poc_runtime.compact.compressed.wasm output 54 | chain-spec-builder -c output/chainspec.json create -n poc-runtime -i poc-runtime -r ./output/poc_runtime.compact.compressed.wasm -s default 55 | cat output/chainspec.json | jq '.properties = {}' > output/chainspec.json.tmp 56 | mv output/chainspec.json.tmp output/chainspec.json 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PVQ 2 | 3 | PolkaVM Query for Polkadot 4 | 5 | ## Getting Started 6 | 7 | ### Prerequisites 8 | 9 | - Pull vendored `polkavm` and patched (for XCM integration PoC) `polkadot-sdk`: `git submodule update --init --recursive`. 10 | - Install `polkatool` (for relinking the standard RV32E ELF to a PolkaVM blob) and `chain-spec-builder` (for building chainspec from a wasm): `make tools` 11 | 12 | ### Run Examples 13 | 14 | `guest-examples` contains several guest programs to test the PVQ. 15 | 16 | 1. Build guest program: `make guests` 17 | 2. Run test runner: `cargo run -p pvq-test-runner -- --program output/` 18 | 19 | Available PoC guest programs: 20 | 21 | - `guest-sum-balance`: sum the balances of multiple accounts 22 | - `guest-total-supply`: get the total supply of an asset 23 | - `guest-sum-balance-percent`: sum the balances of multiple accounts and calculate the percentage of the total supply 24 | 25 | ### RuntimeAPI PoC 26 | 27 | 1. Use chopsticks to start a local chain with the RuntimeAPI enabled: `make run` 28 | 2. Build guest programs: `make guests` 29 | 3. Run test runner to display hex-encoded `args` in tracing logs: `cargo run -p pvq-test-runner -- --program output/` 30 | 4. Upload `program` and `args` in PJS UI. 31 | 32 | ### XCM Integration PoC 33 | 34 | The test case of XCM integration is located in `vendor/polkadot-sdk/polkadot/xcm/xcm-simulator/example/src/tests.rs` 35 | 36 | ```rust 37 | #[test] 38 | fn test_report_query() { 39 | ... 40 | } 41 | ``` 42 | -------------------------------------------------------------------------------- /examples/example-fungibles/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example-fungibles" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | -------------------------------------------------------------------------------- /examples/example-fungibles/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("Hello, world!"); 3 | } 4 | -------------------------------------------------------------------------------- /examples/example-helloworld/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example-helloworld" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | -------------------------------------------------------------------------------- /examples/example-helloworld/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("Hello, world!"); 3 | } 4 | -------------------------------------------------------------------------------- /guest-examples/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | target = "../vendor/polkavm/crates/polkavm-linker/riscv32emac-unknown-none-polkavm.json" 3 | 4 | rustflags = ["-D", "warnings"] 5 | 6 | [unstable] 7 | build-std = ["core", "alloc"] 8 | -------------------------------------------------------------------------------- /guest-examples/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "arrayvec" 7 | version = "0.7.6" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" 10 | 11 | [[package]] 12 | name = "byte-slice-cast" 13 | version = "1.2.3" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d" 16 | 17 | [[package]] 18 | name = "cfg-if" 19 | version = "1.0.0" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 22 | 23 | [[package]] 24 | name = "const_format" 25 | version = "0.2.34" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "126f97965c8ad46d6d9163268ff28432e8f6a1196a55578867832e3049df63dd" 28 | dependencies = [ 29 | "const_format_proc_macros", 30 | ] 31 | 32 | [[package]] 33 | name = "const_format_proc_macros" 34 | version = "0.2.34" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" 37 | dependencies = [ 38 | "proc-macro2", 39 | "quote", 40 | "unicode-xid", 41 | ] 42 | 43 | [[package]] 44 | name = "derive_more" 45 | version = "1.0.0" 46 | source = "registry+https://github.com/rust-lang/crates.io-index" 47 | checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" 48 | dependencies = [ 49 | "derive_more-impl", 50 | ] 51 | 52 | [[package]] 53 | name = "derive_more-impl" 54 | version = "1.0.0" 55 | source = "registry+https://github.com/rust-lang/crates.io-index" 56 | checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" 57 | dependencies = [ 58 | "proc-macro2", 59 | "quote", 60 | "syn", 61 | ] 62 | 63 | [[package]] 64 | name = "equivalent" 65 | version = "1.0.2" 66 | source = "registry+https://github.com/rust-lang/crates.io-index" 67 | checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 68 | 69 | [[package]] 70 | name = "guest-sum-balance" 71 | version = "0.1.0" 72 | dependencies = [ 73 | "cfg-if", 74 | "parity-scale-codec", 75 | "polkavm-derive", 76 | "pvq-program", 77 | ] 78 | 79 | [[package]] 80 | name = "guest-sum-balance-hand-written" 81 | version = "0.1.0" 82 | dependencies = [ 83 | "parity-scale-codec", 84 | "polkavm-derive", 85 | ] 86 | 87 | [[package]] 88 | name = "guest-sum-balance-percent" 89 | version = "0.1.0" 90 | dependencies = [ 91 | "parity-scale-codec", 92 | "polkavm-derive", 93 | "pvq-program", 94 | ] 95 | 96 | [[package]] 97 | name = "guest-test-swap-extension" 98 | version = "0.1.0" 99 | dependencies = [ 100 | "parity-scale-codec", 101 | "polkavm-derive", 102 | "pvq-program", 103 | ] 104 | 105 | [[package]] 106 | name = "guest-total-supply" 107 | version = "0.1.0" 108 | dependencies = [ 109 | "parity-scale-codec", 110 | "polkavm-derive", 111 | "pvq-program", 112 | ] 113 | 114 | [[package]] 115 | name = "guest-total-supply-hand-written" 116 | version = "0.1.0" 117 | dependencies = [ 118 | "parity-scale-codec", 119 | "polkavm-derive", 120 | ] 121 | 122 | [[package]] 123 | name = "guest-transparent-call-hand-written" 124 | version = "0.1.0" 125 | dependencies = [ 126 | "polkavm-derive", 127 | ] 128 | 129 | [[package]] 130 | name = "hashbrown" 131 | version = "0.15.2" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" 134 | 135 | [[package]] 136 | name = "impl-trait-for-tuples" 137 | version = "0.2.3" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9" 140 | dependencies = [ 141 | "proc-macro2", 142 | "quote", 143 | "syn", 144 | ] 145 | 146 | [[package]] 147 | name = "indexmap" 148 | version = "2.8.0" 149 | source = "registry+https://github.com/rust-lang/crates.io-index" 150 | checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" 151 | dependencies = [ 152 | "equivalent", 153 | "hashbrown", 154 | ] 155 | 156 | [[package]] 157 | name = "memchr" 158 | version = "2.7.4" 159 | source = "registry+https://github.com/rust-lang/crates.io-index" 160 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 161 | 162 | [[package]] 163 | name = "parity-scale-codec" 164 | version = "3.7.4" 165 | source = "registry+https://github.com/rust-lang/crates.io-index" 166 | checksum = "c9fde3d0718baf5bc92f577d652001da0f8d54cd03a7974e118d04fc888dc23d" 167 | dependencies = [ 168 | "arrayvec", 169 | "byte-slice-cast", 170 | "const_format", 171 | "impl-trait-for-tuples", 172 | "parity-scale-codec-derive", 173 | "rustversion", 174 | ] 175 | 176 | [[package]] 177 | name = "parity-scale-codec-derive" 178 | version = "3.7.4" 179 | source = "registry+https://github.com/rust-lang/crates.io-index" 180 | checksum = "581c837bb6b9541ce7faa9377c20616e4fb7650f6b0f68bc93c827ee504fb7b3" 181 | dependencies = [ 182 | "proc-macro-crate", 183 | "proc-macro2", 184 | "quote", 185 | "syn", 186 | ] 187 | 188 | [[package]] 189 | name = "polkavm-common" 190 | version = "0.21.0" 191 | 192 | [[package]] 193 | name = "polkavm-derive" 194 | version = "0.21.0" 195 | dependencies = [ 196 | "polkavm-derive-impl-macro", 197 | ] 198 | 199 | [[package]] 200 | name = "polkavm-derive-impl" 201 | version = "0.21.0" 202 | dependencies = [ 203 | "polkavm-common", 204 | "proc-macro2", 205 | "quote", 206 | "syn", 207 | ] 208 | 209 | [[package]] 210 | name = "polkavm-derive-impl-macro" 211 | version = "0.21.0" 212 | dependencies = [ 213 | "polkavm-derive-impl", 214 | "syn", 215 | ] 216 | 217 | [[package]] 218 | name = "proc-macro-crate" 219 | version = "3.3.0" 220 | source = "registry+https://github.com/rust-lang/crates.io-index" 221 | checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" 222 | dependencies = [ 223 | "toml_edit", 224 | ] 225 | 226 | [[package]] 227 | name = "proc-macro2" 228 | version = "1.0.94" 229 | source = "registry+https://github.com/rust-lang/crates.io-index" 230 | checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" 231 | dependencies = [ 232 | "unicode-ident", 233 | ] 234 | 235 | [[package]] 236 | name = "pvq-program" 237 | version = "0.1.0" 238 | dependencies = [ 239 | "parity-scale-codec", 240 | "polkavm-derive", 241 | "pvq-program-procedural", 242 | "scale-info", 243 | ] 244 | 245 | [[package]] 246 | name = "pvq-program-procedural" 247 | version = "0.1.0" 248 | dependencies = [ 249 | "proc-macro-crate", 250 | "proc-macro2", 251 | "quote", 252 | "syn", 253 | ] 254 | 255 | [[package]] 256 | name = "quote" 257 | version = "1.0.40" 258 | source = "registry+https://github.com/rust-lang/crates.io-index" 259 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 260 | dependencies = [ 261 | "proc-macro2", 262 | ] 263 | 264 | [[package]] 265 | name = "rustversion" 266 | version = "1.0.20" 267 | source = "registry+https://github.com/rust-lang/crates.io-index" 268 | checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" 269 | 270 | [[package]] 271 | name = "scale-info" 272 | version = "2.11.6" 273 | source = "registry+https://github.com/rust-lang/crates.io-index" 274 | checksum = "346a3b32eba2640d17a9cb5927056b08f3de90f65b72fe09402c2ad07d684d0b" 275 | dependencies = [ 276 | "cfg-if", 277 | "derive_more", 278 | "parity-scale-codec", 279 | "scale-info-derive", 280 | "serde", 281 | ] 282 | 283 | [[package]] 284 | name = "scale-info-derive" 285 | version = "2.11.6" 286 | source = "registry+https://github.com/rust-lang/crates.io-index" 287 | checksum = "c6630024bf739e2179b91fb424b28898baf819414262c5d376677dbff1fe7ebf" 288 | dependencies = [ 289 | "proc-macro-crate", 290 | "proc-macro2", 291 | "quote", 292 | "syn", 293 | ] 294 | 295 | [[package]] 296 | name = "serde" 297 | version = "1.0.219" 298 | source = "registry+https://github.com/rust-lang/crates.io-index" 299 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 300 | dependencies = [ 301 | "serde_derive", 302 | ] 303 | 304 | [[package]] 305 | name = "serde_derive" 306 | version = "1.0.219" 307 | source = "registry+https://github.com/rust-lang/crates.io-index" 308 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 309 | dependencies = [ 310 | "proc-macro2", 311 | "quote", 312 | "syn", 313 | ] 314 | 315 | [[package]] 316 | name = "syn" 317 | version = "2.0.100" 318 | source = "registry+https://github.com/rust-lang/crates.io-index" 319 | checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" 320 | dependencies = [ 321 | "proc-macro2", 322 | "quote", 323 | "unicode-ident", 324 | ] 325 | 326 | [[package]] 327 | name = "toml_datetime" 328 | version = "0.6.8" 329 | source = "registry+https://github.com/rust-lang/crates.io-index" 330 | checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" 331 | 332 | [[package]] 333 | name = "toml_edit" 334 | version = "0.22.24" 335 | source = "registry+https://github.com/rust-lang/crates.io-index" 336 | checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" 337 | dependencies = [ 338 | "indexmap", 339 | "toml_datetime", 340 | "winnow", 341 | ] 342 | 343 | [[package]] 344 | name = "unicode-ident" 345 | version = "1.0.18" 346 | source = "registry+https://github.com/rust-lang/crates.io-index" 347 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 348 | 349 | [[package]] 350 | name = "unicode-xid" 351 | version = "0.2.6" 352 | source = "registry+https://github.com/rust-lang/crates.io-index" 353 | checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" 354 | 355 | [[package]] 356 | name = "winnow" 357 | version = "0.7.4" 358 | source = "registry+https://github.com/rust-lang/crates.io-index" 359 | checksum = "0e97b544156e9bebe1a0ffbc03484fc1ffe3100cbce3ffb17eac35f7cdd7ab36" 360 | dependencies = [ 361 | "memchr", 362 | ] 363 | -------------------------------------------------------------------------------- /guest-examples/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "sum-balance", 4 | "sum-balance-percent", 5 | "sum-balance-hand-written", 6 | "total-supply", 7 | "total-supply-hand-written", 8 | "transparent-call-hand-written", 9 | "test-swap-extension", 10 | ] 11 | resolver = "2" 12 | 13 | [workspace.dependencies] 14 | parity-scale-codec = { version = "3", default-features = false, features = [ 15 | "derive", 16 | ] } 17 | pvq-program = { path = "../pvq-program", default-features = false } 18 | pvq-program-metadata-gen = { path = "../pvq-program-metadata-gen" } 19 | polkavm-derive = { path = "../vendor/polkavm/crates/polkavm-derive" } 20 | cfg-if = "1.0" 21 | -------------------------------------------------------------------------------- /guest-examples/rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "nightly-2024-11-19" 3 | components = ["rust-src"] 4 | -------------------------------------------------------------------------------- /guest-examples/sum-balance-hand-written/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "guest-sum-balance-hand-written" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [dependencies] 8 | polkavm-derive = { workspace = true } 9 | parity-scale-codec = { workspace = true, features = ["derive"] } 10 | -------------------------------------------------------------------------------- /guest-examples/sum-balance-hand-written/src/main.rs: -------------------------------------------------------------------------------- 1 | // Generated by macro 2 | #![no_std] 3 | #![no_main] 4 | extern crate alloc; 5 | use alloc::vec::Vec; 6 | 7 | #[global_allocator] 8 | static GLOBAL: polkavm_derive::LeakingAllocator = polkavm_derive::LeakingAllocator; 9 | 10 | #[panic_handler] 11 | fn panic(_info: &core::panic::PanicInfo) -> ! { 12 | unsafe { 13 | core::arch::asm!("unimp", options(noreturn)); 14 | } 15 | } 16 | 17 | #[polkavm_derive::polkavm_import] 18 | extern "C" { 19 | fn host_call(extension_id: u64, call_ptr: u32, call_len: u32) -> u64; 20 | } 21 | 22 | type AssetId = u32; 23 | type AccountId = [u8; 32]; 24 | type Balance = u64; 25 | 26 | // #[program::extension(extension_id = 4071833530116166512u64, fn_index = 1)] 27 | // fn balance(asset: AssetId, who: AccountId) -> Balance {} 28 | // expands to 29 | #[allow(non_camel_case_types)] 30 | #[derive(parity_scale_codec::Encode, parity_scale_codec::Decode)] 31 | enum BalanceCall { 32 | #[codec(index = 1)] 33 | balance { asset: AssetId, who: AccountId }, 34 | } 35 | fn balance(asset: AssetId, who: AccountId) -> Balance { 36 | let encoded_call = parity_scale_codec::Encode::encode(&BalanceCall::balance { asset, who }); 37 | let res = unsafe { 38 | host_call( 39 | 4071833530116166512u64, 40 | encoded_call.as_ptr() as u32, 41 | encoded_call.len() as u32, 42 | ) 43 | }; 44 | let res_ptr = res as u32 as *const u8; 45 | let res_len = (res >> 32) as usize; 46 | let mut res_bytes = unsafe { core::slice::from_raw_parts(res_ptr, res_len) }; 47 | parity_scale_codec::Decode::decode(&mut res_bytes).expect("Failed to decode result") 48 | } 49 | 50 | // #[program::entrypoint] 51 | // fn sum_balance(asset: AssetId, accounts: Vec) -> Balance { 52 | // let mut sum = 0; 53 | // for account in accounts { 54 | // sum += balance(asset, account); 55 | // } 56 | // sum 57 | // } 58 | // expands to 59 | #[polkavm_derive::polkavm_export] 60 | extern "C" fn main(arg_ptr: u32, size: u32) -> u64 { 61 | // Decode the arguments 62 | let mut arg_bytes = unsafe { core::slice::from_raw_parts(arg_ptr as *const u8, size as usize) }; 63 | let asset: AssetId = parity_scale_codec::Decode::decode(&mut arg_bytes).expect("Failed to decode asset"); 64 | let accounts: Vec = 65 | parity_scale_codec::Decode::decode(&mut arg_bytes).expect("Failed to decode accounts"); 66 | 67 | // Call the function 68 | let res = sum_balance(asset, accounts); 69 | 70 | // Encode the result 71 | let encoded_res = parity_scale_codec::Encode::encode(&res); 72 | 73 | // Return the result 74 | (encoded_res.len() as u64) << 32 | (encoded_res.as_ptr() as u64) 75 | } 76 | 77 | fn sum_balance(asset: AssetId, accounts: Vec) -> Balance { 78 | let mut sum = 0; 79 | for account in accounts { 80 | sum += balance(asset, account); 81 | } 82 | sum 83 | } 84 | -------------------------------------------------------------------------------- /guest-examples/sum-balance-percent/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "guest-sum-balance-percent" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [dependencies] 8 | polkavm-derive = { workspace = true } 9 | pvq-program = { workspace = true } 10 | parity-scale-codec = { workspace = true } 11 | -------------------------------------------------------------------------------- /guest-examples/sum-balance-percent/build.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::path::PathBuf; 3 | use std::process::Command; 4 | 5 | fn main() { 6 | // Tell Cargo to rerun this build script if the source file changes 7 | println!("cargo:rerun-if-changed=src/main.rs"); 8 | let current_dir = env::current_dir().expect("Failed to get current directory"); 9 | // Determine the output directory for the metadata 10 | let output_dir = PathBuf::from(env::var("METADATA_OUTPUT_DIR").expect("METADATA_OUTPUT_DIR is not set")); 11 | 12 | // Build and run the command 13 | let status = Command::new("pvq-program-metadata-gen") 14 | .arg("--crate-path") 15 | .arg(¤t_dir) 16 | .arg("--output-dir") 17 | .arg(&output_dir) 18 | .env("RUST_LOG", "info") 19 | .status() 20 | .expect("Failed to execute pvq-program-metadata-gen"); 21 | 22 | if !status.success() { 23 | panic!("Failed to generate program metadata"); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /guest-examples/sum-balance-percent/src/main.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![no_main] 3 | 4 | #[pvq_program::program] 5 | mod sum_balance_percent { 6 | type AssetId = u32; 7 | type AccountId = [u8; 32]; 8 | type Balance = u64; 9 | #[program::extension_fn(extension_id = 4071833530116166512u64, fn_index = 1)] 10 | fn balance(asset: AssetId, who: AccountId) -> Balance {} 11 | #[program::extension_fn(extension_id = 4071833530116166512u64, fn_index = 0)] 12 | fn total_supply(asset: AssetId) -> Balance {} 13 | 14 | #[program::entrypoint] 15 | fn sum_balance(asset: AssetId, accounts: alloc::vec::Vec) -> Balance { 16 | let mut sum_balance = 0; 17 | for account in accounts { 18 | sum_balance += balance(asset, account); 19 | } 20 | sum_balance * 100 / total_supply(asset) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /guest-examples/sum-balance/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "guest-sum-balance" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [dependencies] 8 | parity-scale-codec = { workspace = true } 9 | polkavm-derive = { workspace = true } 10 | pvq-program = { workspace = true } 11 | cfg-if = { workspace = true } 12 | 13 | [features] 14 | option_version_1 = [] 15 | option_version_2 = [] 16 | -------------------------------------------------------------------------------- /guest-examples/sum-balance/build.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::path::PathBuf; 3 | use std::process::Command; 4 | 5 | fn main() { 6 | // Tell Cargo to rerun this build script if the source file changes 7 | println!("cargo:rerun-if-changed=src/main.rs"); 8 | let current_dir = env::current_dir().expect("Failed to get current directory"); 9 | // Determine the output directory for the metadata 10 | let output_dir = PathBuf::from(env::var("METADATA_OUTPUT_DIR").expect("METADATA_OUTPUT_DIR is not set")) 11 | .canonicalize() 12 | .expect("Failed to canonicalize output directory"); 13 | 14 | // Build and run the command 15 | let status = Command::new("pvq-program-metadata-gen") 16 | .arg("--crate-path") 17 | .arg(¤t_dir) 18 | .arg("--output-dir") 19 | .arg(&output_dir) 20 | .env("RUST_LOG", "info") 21 | .status() 22 | .expect("Failed to execute pvq-program-metadata-gen"); 23 | 24 | if !status.success() { 25 | panic!("Failed to generate program metadata"); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /guest-examples/sum-balance/src/main.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![no_main] 3 | 4 | #[pvq_program::program] 5 | mod sum_balance { 6 | 7 | cfg_if::cfg_if! { 8 | if #[cfg(feature = "option_version_1")] { 9 | type AccountId = [u8; 64]; 10 | type AssetId = u64; 11 | type Balance = u128; 12 | } else if #[cfg(feature = "option_version_2")] { 13 | type AccountId = [u8; 32]; 14 | type AssetId = u32; 15 | type Balance = u64; 16 | } else { 17 | type AccountId = [u8; 32]; 18 | type AssetId = u32; 19 | type Balance = u64; 20 | } 21 | } 22 | 23 | #[program::extension_fn(extension_id = 4071833530116166512u64, fn_index = 1)] 24 | fn balance(asset: AssetId, who: AccountId) -> Balance {} 25 | 26 | #[program::entrypoint] 27 | fn sum_balance(asset: AssetId, accounts: alloc::vec::Vec) -> Balance { 28 | let mut sum = 0; 29 | for account in accounts { 30 | sum += balance(asset, account); 31 | } 32 | sum 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /guest-examples/test-swap-extension/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "guest-test-swap-extension" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | parity-scale-codec = { workspace = true } 8 | polkavm-derive = { workspace = true } 9 | pvq-program = { workspace = true } 10 | -------------------------------------------------------------------------------- /guest-examples/test-swap-extension/src/main.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![no_main] 3 | 4 | #[pvq_program::program] 5 | mod query_pools { 6 | 7 | type AssetId = alloc::vec::Vec; 8 | type Balance = u128; 9 | const UNITS: Balance = 1_000_000_000_000; 10 | 11 | #[program::extension_fn(extension_id = 13206387959972970661u64, fn_index = 0)] 12 | fn quote_price_tokens_for_exact_tokens( 13 | asset1: AssetId, 14 | asset2: AssetId, 15 | amount: Balance, 16 | include_fee: bool, 17 | ) -> Option { 18 | } 19 | 20 | #[program::extension_fn(extension_id = 13206387959972970661u64, fn_index = 1)] 21 | fn quote_price_exact_tokens_for_tokens( 22 | asset1: AssetId, 23 | asset2: AssetId, 24 | amount: Balance, 25 | include_fee: bool, 26 | ) -> Option { 27 | } 28 | 29 | #[program::extension_fn(extension_id = 13206387959972970661u64, fn_index = 2)] 30 | fn get_liquidity_pool(asset1: AssetId, asset2: AssetId) -> Option<(Balance, Balance)> {} 31 | 32 | #[program::extension_fn(extension_id = 13206387959972970661u64, fn_index = 3)] 33 | fn list_pools() -> alloc::vec::Vec<(AssetId, AssetId, Balance, Balance)> {} 34 | 35 | #[program::entrypoint] 36 | fn test_swap_extension(asset1: AssetId, asset2: AssetId) -> Option<(Balance, Balance)> { 37 | // Check quote prices 38 | let amount_in = quote_price_tokens_for_exact_tokens(asset1.clone(), asset2.clone(), 10 * UNITS, false) 39 | .expect("Quote price exists"); 40 | 41 | assert!(amount_in == 20 * UNITS); 42 | let amount_out = quote_price_exact_tokens_for_tokens(asset1.clone(), asset2.clone(), 20 * UNITS, false) 43 | .expect("Quote price exists"); 44 | assert!(amount_out == 10 * UNITS); 45 | 46 | // // Check list_pools 47 | let pools = list_pools(); 48 | assert!(pools.len() == 1); 49 | let (pool_asset1, pool_asset2, pool_balance1, pool_balance2) = &pools[0]; 50 | assert!(pool_asset1 == &asset1); 51 | assert!(pool_asset2 == &asset2); 52 | assert!(*pool_balance1 == 100 * UNITS); 53 | assert!(*pool_balance2 == 50 * UNITS); 54 | 55 | // Check get_liquidity_pool 56 | let (balance1, balance2) = get_liquidity_pool(asset1, asset2).expect("Pool exists"); 57 | Some((balance1, balance2)) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /guest-examples/total-supply-hand-written/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "guest-total-supply-hand-written" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | polkavm-derive = { workspace = true } 10 | parity-scale-codec = { workspace = true, features = ["derive"] } 11 | -------------------------------------------------------------------------------- /guest-examples/total-supply-hand-written/src/main.rs: -------------------------------------------------------------------------------- 1 | // Generated by macro 2 | #![no_std] 3 | #![no_main] 4 | extern crate alloc; 5 | 6 | #[global_allocator] 7 | static GLOBAL: polkavm_derive::LeakingAllocator = polkavm_derive::LeakingAllocator; 8 | 9 | #[panic_handler] 10 | fn panic(_info: &core::panic::PanicInfo) -> ! { 11 | unsafe { 12 | core::arch::asm!("unimp", options(noreturn)); 13 | } 14 | } 15 | 16 | #[polkavm_derive::polkavm_import] 17 | extern "C" { 18 | fn host_call(extension_id: u64, call_ptr: u32, call_len: u32) -> u64; 19 | } 20 | 21 | type AssetId = u32; 22 | type Balance = u64; 23 | 24 | // #[program::extension(extension_id = 4071833530116166512u64, call_index = 1)] 25 | // fn total_supply(asset: AssetId) -> Balance {} 26 | // expands to 27 | #[allow(non_camel_case_types)] 28 | #[derive(parity_scale_codec::Encode, parity_scale_codec::Decode)] 29 | enum TotalSupplyCall { 30 | #[codec(index = 0)] 31 | total_supply { asset: AssetId }, 32 | } 33 | fn total_supply(asset: AssetId) -> Balance { 34 | let encoded_call = parity_scale_codec::Encode::encode(&TotalSupplyCall::total_supply { asset }); 35 | let res = unsafe { 36 | host_call( 37 | 4071833530116166512u64, 38 | encoded_call.as_ptr() as u32, 39 | encoded_call.len() as u32, 40 | ) 41 | }; 42 | let res_ptr = res as u32 as *const u8; 43 | let res_len = (res >> 32) as usize; 44 | let mut res_bytes = unsafe { core::slice::from_raw_parts(res_ptr, res_len) }; 45 | parity_scale_codec::Decode::decode(&mut res_bytes).expect("Failed to decode result") 46 | } 47 | 48 | // #[program::entrypoint] 49 | // fn total_supply_(asset: AssetId) -> Balance { 50 | // total_supply(asset) 51 | // } 52 | // expands to 53 | #[polkavm_derive::polkavm_export] 54 | extern "C" fn main(arg_ptr: u32, size: u32) -> u64 { 55 | // Decode the arguments 56 | let mut arg_bytes = unsafe { core::slice::from_raw_parts(arg_ptr as *const u8, size as usize) }; 57 | let asset: AssetId = parity_scale_codec::Decode::decode(&mut arg_bytes).expect("Failed to decode asset"); 58 | 59 | // Call the function 60 | let res = total_supply_(asset); 61 | 62 | // Encode the result 63 | let encoded_res = parity_scale_codec::Encode::encode(&res); 64 | 65 | // Return the result 66 | (encoded_res.len() as u64) << 32 | (encoded_res.as_ptr() as u64) 67 | } 68 | 69 | fn total_supply_(asset: AssetId) -> Balance { 70 | total_supply(asset) 71 | } 72 | -------------------------------------------------------------------------------- /guest-examples/total-supply/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "guest-total-supply" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | polkavm-derive = { workspace = true } 10 | pvq-program = { workspace = true } 11 | parity-scale-codec = { workspace = true } 12 | -------------------------------------------------------------------------------- /guest-examples/total-supply/src/main.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![no_main] 3 | 4 | #[pvq_program::program] 5 | mod query_total_supply { 6 | #[program::extension_fn(extension_id = 4071833530116166512u64, fn_index = 0)] 7 | fn total_supply(asset: u32) -> u64 {} 8 | 9 | #[program::entrypoint] 10 | fn get_total_supply(asset: u32) -> u64 { 11 | total_supply(asset) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /guest-examples/transparent-call-hand-written/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "guest-transparent-call-hand-written" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [dependencies] 8 | polkavm-derive = { workspace = true } 9 | -------------------------------------------------------------------------------- /guest-examples/transparent-call-hand-written/src/main.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![no_main] 3 | 4 | #[panic_handler] 5 | fn panic(_info: &core::panic::PanicInfo) -> ! { 6 | unsafe { 7 | core::arch::asm!("unimp", options(noreturn)); 8 | } 9 | } 10 | 11 | #[polkavm_derive::polkavm_import] 12 | extern "C" { 13 | fn host_call(extension_id: u64, call_ptr: u32, call_len: u32) -> u64; 14 | } 15 | 16 | #[polkavm_derive::polkavm_export] 17 | extern "C" fn main(ptr: u32, size: u32) -> u64 { 18 | let extension_id = unsafe { core::ptr::read_volatile(ptr as *const u64) }; 19 | unsafe { host_call(extension_id, ptr + 8, size - 8) } 20 | } 21 | -------------------------------------------------------------------------------- /poc/runtime/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "poc-runtime" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | parity-scale-codec = { workspace = true } 8 | scale-info = { workspace = true } 9 | frame = { workspace = true, features = ["experimental", "runtime"] } 10 | 11 | # pallets that we want to use 12 | pallet-balances = { workspace = true } 13 | pallet-assets = { workspace = true } 14 | pallet-sudo = { workspace = true } 15 | pallet-timestamp = { workspace = true } 16 | pallet-transaction-payment = { workspace = true } 17 | pallet-transaction-payment-rpc-runtime-api = { workspace = true } 18 | 19 | # genesis builder that allows us to interacto with runtime genesis config 20 | sp-genesis-builder = { workspace = true } 21 | 22 | pvq-executor = { workspace = true } 23 | pvq-extension = { workspace = true } 24 | pvq-extension-core = { workspace = true } 25 | pvq-extension-fungibles = { workspace = true } 26 | pvq-primitives = { workspace = true } 27 | pvq-runtime-api = { workspace = true } 28 | 29 | [dev-dependencies] 30 | hex = "0.4" 31 | 32 | [build-dependencies] 33 | substrate-wasm-builder = { workspace = true, optional = true } 34 | 35 | [features] 36 | default = ["std"] 37 | std = [ 38 | "parity-scale-codec/std", 39 | "scale-info/std", 40 | "frame/std", 41 | 42 | "pallet-balances/std", 43 | "pallet-assets/std", 44 | "pallet-sudo/std", 45 | "pallet-timestamp/std", 46 | "pallet-transaction-payment-rpc-runtime-api/std", 47 | "pallet-transaction-payment/std", 48 | 49 | "sp-genesis-builder/std", 50 | "substrate-wasm-builder", 51 | 52 | "pvq-executor/std", 53 | "pvq-extension/std", 54 | "pvq-primitives/std", 55 | "pvq-extension-core/std", 56 | "pvq-extension-fungibles/std", 57 | "pvq-runtime-api/std", 58 | ] 59 | -------------------------------------------------------------------------------- /poc/runtime/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | #[cfg(feature = "std")] 3 | { 4 | substrate_wasm_builder::WasmBuilder::build_using_defaults(); 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /poc/runtime/chopsticks.yml: -------------------------------------------------------------------------------- 1 | mock-signature-host: true 2 | runtime-log-level: 4 3 | import-storage: 4 | Sudo: 5 | Key: 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY # Alice 6 | 7 | System: 8 | Account: 9 | - 10 | - 11 | - 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY 12 | - providers: 1 13 | data: 14 | free: "10000000000000000000" 15 | Assets: 16 | Account: 17 | - [[1984, 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY], { balance: 1000000000 }] 18 | - [[21, 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY], { balance: 1000000000 }] 19 | Asset: [[[21], { supply: 1000000000 }]] 20 | -------------------------------------------------------------------------------- /poc/runtime/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(not(feature = "std"), no_std)] 2 | 3 | // Make the WASM binary available. 4 | #[cfg(feature = "std")] 5 | include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs")); 6 | 7 | extern crate alloc; 8 | use alloc::{vec, vec::Vec}; 9 | 10 | mod pvq; 11 | 12 | use frame::{ 13 | deps::frame_support::{ 14 | genesis_builder_helper::{build_state, get_preset}, 15 | weights::{FixedFee, NoFee}, 16 | }, 17 | prelude::*, 18 | runtime::{ 19 | apis::{self, impl_runtime_apis}, 20 | prelude::*, 21 | }, 22 | }; 23 | 24 | #[runtime_version] 25 | pub const VERSION: RuntimeVersion = RuntimeVersion { 26 | spec_name: alloc::borrow::Cow::Borrowed("pvq-poc"), 27 | impl_name: alloc::borrow::Cow::Borrowed("pvq-poc"), 28 | authoring_version: 1, 29 | spec_version: 0, 30 | impl_version: 1, 31 | apis: RUNTIME_API_VERSIONS, 32 | transaction_version: 1, 33 | system_version: 1, 34 | }; 35 | 36 | /// The version information used to identify this runtime when compiled natively. 37 | #[cfg(feature = "std")] 38 | pub fn native_version() -> NativeVersion { 39 | NativeVersion { 40 | runtime_version: VERSION, 41 | can_author_with: Default::default(), 42 | } 43 | } 44 | 45 | type SignedExtra = ( 46 | frame_system::CheckNonZeroSender, 47 | frame_system::CheckSpecVersion, 48 | frame_system::CheckTxVersion, 49 | frame_system::CheckGenesis, 50 | frame_system::CheckEra, 51 | frame_system::CheckNonce, 52 | frame_system::CheckWeight, 53 | pallet_transaction_payment::ChargeTransactionPayment, 54 | ); 55 | 56 | construct_runtime!( 57 | pub enum Runtime { 58 | System: frame_system, 59 | Timestamp: pallet_timestamp, 60 | 61 | Assets: pallet_assets, 62 | Balances: pallet_balances, 63 | Sudo: pallet_sudo, 64 | TransactionPayment: pallet_transaction_payment, 65 | } 66 | ); 67 | 68 | parameter_types! { 69 | pub const Version: RuntimeVersion = VERSION; 70 | } 71 | 72 | #[derive_impl(frame_system::config_preludes::SolochainDefaultConfig as frame_system::DefaultConfig)] 73 | impl frame_system::Config for Runtime { 74 | type Block = Block; 75 | type Version = Version; 76 | type BlockHashCount = ConstU32<1024>; 77 | type AccountData = pallet_balances::AccountData<::Balance>; 78 | } 79 | 80 | #[derive_impl(pallet_balances::config_preludes::TestDefaultConfig as pallet_balances::DefaultConfig)] 81 | impl pallet_balances::Config for Runtime { 82 | type AccountStore = System; 83 | } 84 | 85 | #[derive_impl(pallet_assets::config_preludes::TestDefaultConfig as pallet_assets::DefaultConfig)] 86 | impl pallet_assets::Config for Runtime { 87 | type Currency = Balances; 88 | type ForceOrigin = frame_system::EnsureRoot; 89 | type CreateOrigin = AsEnsureOriginWithArg>; 90 | type Freezer = (); 91 | } 92 | 93 | #[derive_impl(pallet_sudo::config_preludes::TestDefaultConfig as pallet_sudo::DefaultConfig)] 94 | impl pallet_sudo::Config for Runtime {} 95 | 96 | #[derive_impl(pallet_timestamp::config_preludes::TestDefaultConfig as pallet_timestamp::DefaultConfig)] 97 | impl pallet_timestamp::Config for Runtime {} 98 | 99 | #[derive_impl(pallet_transaction_payment::config_preludes::TestDefaultConfig as pallet_transaction_payment::DefaultConfig)] 100 | impl pallet_transaction_payment::Config for Runtime { 101 | type OnChargeTransaction = pallet_transaction_payment::FungibleAdapter; 102 | type WeightToFee = NoFee<::Balance>; 103 | type LengthToFee = FixedFee<1, ::Balance>; 104 | } 105 | 106 | type Block = frame::runtime::types_common::BlockOf; 107 | type Header = HeaderFor; 108 | 109 | type RuntimeExecutive = Executive, Runtime, AllPalletsWithSystem>; 110 | 111 | use pallet_transaction_payment::{FeeDetails, RuntimeDispatchInfo}; 112 | 113 | const ONE_SECOND_IN_GAS: i64 = 100000; 114 | 115 | impl_runtime_apis! { 116 | impl apis::Core for Runtime { 117 | fn version() -> RuntimeVersion { 118 | VERSION 119 | } 120 | 121 | fn execute_block(block: Block) { 122 | RuntimeExecutive::execute_block(block) 123 | } 124 | 125 | fn initialize_block(header: &Header) -> ExtrinsicInclusionMode { 126 | RuntimeExecutive::initialize_block(header) 127 | } 128 | } 129 | impl apis::Metadata for Runtime { 130 | fn metadata() -> OpaqueMetadata { 131 | OpaqueMetadata::new(Runtime::metadata().into()) 132 | } 133 | 134 | fn metadata_at_version(version: u32) -> Option { 135 | Runtime::metadata_at_version(version) 136 | } 137 | 138 | fn metadata_versions() -> Vec { 139 | Runtime::metadata_versions() 140 | } 141 | } 142 | 143 | impl apis::BlockBuilder for Runtime { 144 | fn apply_extrinsic(extrinsic: ExtrinsicFor) -> ApplyExtrinsicResult { 145 | RuntimeExecutive::apply_extrinsic(extrinsic) 146 | } 147 | 148 | fn finalize_block() -> HeaderFor { 149 | RuntimeExecutive::finalize_block() 150 | } 151 | 152 | fn inherent_extrinsics(data: InherentData) -> Vec> { 153 | data.create_extrinsics() 154 | } 155 | 156 | fn check_inherents( 157 | block: Block, 158 | data: InherentData, 159 | ) -> CheckInherentsResult { 160 | data.check_extrinsics(&block) 161 | } 162 | } 163 | 164 | impl apis::TaggedTransactionQueue for Runtime { 165 | fn validate_transaction( 166 | source: TransactionSource, 167 | tx: ExtrinsicFor, 168 | block_hash: ::Hash, 169 | ) -> TransactionValidity { 170 | RuntimeExecutive::validate_transaction(source, tx, block_hash) 171 | } 172 | } 173 | 174 | impl apis::OffchainWorkerApi for Runtime { 175 | fn offchain_worker(header: &HeaderFor) { 176 | RuntimeExecutive::offchain_worker(header) 177 | } 178 | } 179 | 180 | impl apis::SessionKeys for Runtime { 181 | fn generate_session_keys(_seed: Option>) -> Vec { 182 | Default::default() 183 | } 184 | 185 | fn decode_session_keys( 186 | _encoded: Vec, 187 | ) -> Option, apis::KeyTypeId)>> { 188 | Default::default() 189 | } 190 | } 191 | 192 | impl apis::AccountNonceApi for Runtime { 193 | fn account_nonce(account: interface::AccountId) -> interface::Nonce { 194 | System::account_nonce(account) 195 | } 196 | } 197 | 198 | impl pallet_transaction_payment_rpc_runtime_api::TransactionPaymentApi< 199 | Block, 200 | interface::Balance, 201 | > for Runtime { 202 | fn query_info(uxt: ExtrinsicFor, len: u32) -> RuntimeDispatchInfo { 203 | TransactionPayment::query_info(uxt, len) 204 | } 205 | fn query_fee_details(uxt: ExtrinsicFor, len: u32) -> FeeDetails { 206 | TransactionPayment::query_fee_details(uxt, len) 207 | } 208 | fn query_weight_to_fee(weight: Weight) -> interface::Balance { 209 | TransactionPayment::weight_to_fee(weight) 210 | } 211 | fn query_length_to_fee(length: u32) -> interface::Balance { 212 | TransactionPayment::length_to_fee(length) 213 | } 214 | } 215 | 216 | impl sp_genesis_builder::GenesisBuilder for Runtime { 217 | fn build_state(config: Vec) -> sp_genesis_builder::Result { 218 | build_state::(config) 219 | } 220 | 221 | fn get_preset(id: &Option) -> Option> { 222 | get_preset::(id, |_| None) 223 | } 224 | 225 | fn preset_names() -> Vec { 226 | vec![] 227 | } 228 | } 229 | 230 | impl pvq_runtime_api::PvqApi for Runtime { 231 | fn execute_query(program: Vec, args: Vec, gas_limit: Option) -> pvq_primitives::PvqResult { 232 | // Set a default gas limit of 2 seconds 233 | pvq::execute_query(&program, &args, gas_limit.unwrap_or(ONE_SECOND_IN_GAS * 2)) 234 | } 235 | fn metadata() -> Vec { 236 | pvq::metadata().encode() 237 | } 238 | } 239 | } 240 | 241 | /// Some re-exports that the node side code needs to know. Some are useful in this context as well. 242 | /// 243 | /// Other types should preferably be private. 244 | // TODO: this should be standardized in some way, see: 245 | // https://github.com/paritytech/substrate/issues/10579#issuecomment-1600537558 246 | pub mod interface { 247 | use super::Runtime; 248 | use frame::deps::frame_system; 249 | 250 | pub type Block = super::Block; 251 | pub use frame::runtime::types_common::OpaqueBlock; 252 | pub type AssetId = ::AssetId; 253 | pub type AccountId = ::AccountId; 254 | pub type Nonce = ::Nonce; 255 | pub type Hash = ::Hash; 256 | pub type Balance = ::Balance; 257 | pub type MinimumBalance = ::ExistentialDeposit; 258 | } 259 | -------------------------------------------------------------------------------- /poc/runtime/src/pvq.rs: -------------------------------------------------------------------------------- 1 | #[allow(unused_imports)] 2 | use frame::deps::scale_info::prelude::{format, string::String}; 3 | 4 | use pvq_extension::metadata::Metadata; 5 | use pvq_extension::{extensions_impl, ExtensionsExecutor, InvokeSource}; 6 | 7 | #[extensions_impl] 8 | pub mod extensions { 9 | #[extensions_impl::impl_struct] 10 | pub struct ExtensionImpl; 11 | 12 | #[extensions_impl::extension] 13 | impl pvq_extension_core::extension::ExtensionCore for ExtensionImpl { 14 | type ExtensionId = u64; 15 | fn has_extension(id: Self::ExtensionId) -> bool { 16 | id == pvq_extension_core::extension::extension_id() 17 | || id == pvq_extension_fungibles::extension::extension_id() 18 | } 19 | } 20 | 21 | #[extensions_impl::extension] 22 | impl pvq_extension_fungibles::extension::ExtensionFungibles for ExtensionImpl { 23 | type AccountId = [u8; 32]; 24 | type Balance = crate::interface::Balance; 25 | type AssetId = crate::interface::AssetId; 26 | fn balance(asset: Self::AssetId, who: Self::AccountId) -> Self::Balance { 27 | crate::Assets::balance(asset, crate::interface::AccountId::from(who)) 28 | } 29 | fn total_supply(asset: Self::AssetId) -> Self::Balance { 30 | crate::Assets::total_supply(asset) 31 | } 32 | } 33 | } 34 | 35 | pub fn execute_query(program: &[u8], args: &[u8], gas_limit: i64) -> pvq_primitives::PvqResult { 36 | let mut executor = ExtensionsExecutor::::new(InvokeSource::RuntimeAPI); 37 | let (result, _) = executor.execute(program, args, Some(gas_limit)); 38 | result 39 | } 40 | 41 | pub fn metadata() -> Metadata { 42 | extensions::metadata() 43 | } 44 | -------------------------------------------------------------------------------- /pvq-executor/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pvq-executor" 3 | description = "PVQ program executor" 4 | authors.workspace = true 5 | edition.workspace = true 6 | license.workspace = true 7 | repository.workspace = true 8 | version.workspace = true 9 | 10 | [dependencies] 11 | tracing = { workspace = true } 12 | 13 | polkavm = { workspace = true } 14 | 15 | pvq-primitives = { workspace = true } 16 | 17 | [features] 18 | default = ["std"] 19 | std = ["tracing/std", "polkavm/std", "pvq-primitives/std"] 20 | -------------------------------------------------------------------------------- /pvq-executor/src/context.rs: -------------------------------------------------------------------------------- 1 | use polkavm::Linker; 2 | 3 | pub trait PvqExecutorContext { 4 | type UserData; 5 | type UserError; 6 | fn register_host_functions(&mut self, linker: &mut Linker); 7 | fn data(&mut self) -> &mut Self::UserData; 8 | } 9 | -------------------------------------------------------------------------------- /pvq-executor/src/error.rs: -------------------------------------------------------------------------------- 1 | use pvq_primitives::PvqError; 2 | #[derive(Debug)] 3 | pub enum PvqExecutorError { 4 | InvalidProgramFormat, 5 | MemoryAccessError(polkavm::MemoryAccessError), 6 | // Extract from the PVM CallError 7 | Trap, 8 | // Extract from the PVM CallError 9 | NotEnoughGas, 10 | // Usually a custom error type from the extension system definition 11 | User(UserError), 12 | // Other errors directly from the PVM 13 | OtherPvmError(polkavm::Error), 14 | } 15 | 16 | impl From for PvqExecutorError { 17 | fn from(err: polkavm::MemoryAccessError) -> Self { 18 | Self::MemoryAccessError(err) 19 | } 20 | } 21 | 22 | impl From for PvqExecutorError { 23 | fn from(err: polkavm::Error) -> Self { 24 | Self::OtherPvmError(err) 25 | } 26 | } 27 | 28 | impl From> for PvqExecutorError { 29 | fn from(err: polkavm::CallError) -> Self { 30 | match err { 31 | polkavm::CallError::Trap => Self::Trap, 32 | polkavm::CallError::NotEnoughGas => Self::NotEnoughGas, 33 | polkavm::CallError::Error(e) => Self::OtherPvmError(e), 34 | polkavm::CallError::User(e) => Self::User(e), 35 | } 36 | } 37 | } 38 | 39 | impl From> for PvqError { 40 | fn from(e: PvqExecutorError) -> PvqError { 41 | match e { 42 | PvqExecutorError::InvalidProgramFormat => PvqError::InvalidPvqProgramFormat, 43 | PvqExecutorError::MemoryAccessError(_) => PvqError::MemoryAccessError, 44 | PvqExecutorError::Trap => PvqError::Trap, 45 | PvqExecutorError::NotEnoughGas => PvqError::QueryExceedsWeightLimit, 46 | PvqExecutorError::User(_) => PvqError::HostCallError, 47 | PvqExecutorError::OtherPvmError(_) => PvqError::Other, 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /pvq-executor/src/executor.rs: -------------------------------------------------------------------------------- 1 | use alloc::vec::Vec; 2 | use polkavm::{Config, Engine, Linker, Module, ModuleConfig, ProgramBlob}; 3 | 4 | use crate::context::PvqExecutorContext; 5 | use crate::error::PvqExecutorError; 6 | 7 | type PvqExecutorResult = Result, PvqExecutorError>; 8 | type GasLimit = Option; 9 | 10 | pub struct PvqExecutor { 11 | engine: Engine, 12 | linker: Linker, 13 | context: Ctx, 14 | } 15 | 16 | impl PvqExecutor { 17 | pub fn new(config: Config, mut context: Ctx) -> Self { 18 | let engine = Engine::new(&config).unwrap(); 19 | let mut linker = Linker::::new(); 20 | // Register user-defined host functions 21 | context.register_host_functions(&mut linker); 22 | Self { 23 | engine, 24 | linker, 25 | context, 26 | } 27 | } 28 | 29 | pub fn execute( 30 | &mut self, 31 | program: &[u8], 32 | args: &[u8], 33 | gas_limit: GasLimit, 34 | ) -> (PvqExecutorResult, GasLimit) { 35 | let blob = match ProgramBlob::parse(program.into()) { 36 | Ok(blob) => blob, 37 | Err(_) => return (Err(PvqExecutorError::InvalidProgramFormat), gas_limit), 38 | }; 39 | 40 | let mut module_config = ModuleConfig::new(); 41 | module_config.set_aux_data_size(args.len() as u32); 42 | if gas_limit.is_some() { 43 | module_config.set_gas_metering(Some(polkavm::GasMeteringKind::Sync)); 44 | } 45 | 46 | let module = match Module::from_blob(&self.engine, &module_config, blob) { 47 | Ok(module) => module, 48 | Err(err) => return (Err(err.into()), gas_limit), 49 | }; 50 | 51 | let instance_pre = match self.linker.instantiate_pre(&module) { 52 | Ok(instance_pre) => instance_pre, 53 | Err(err) => return (Err(err.into()), gas_limit), 54 | }; 55 | 56 | let mut instance = match instance_pre.instantiate() { 57 | Ok(instance) => instance, 58 | Err(err) => return (Err(err.into()), gas_limit), 59 | }; 60 | 61 | if let Some(gas_limit) = gas_limit { 62 | instance.set_gas(gas_limit); 63 | } 64 | 65 | // From this point on, we include instance.gas() in the return value 66 | let result = (|| { 67 | instance.write_memory(module.memory_map().aux_data_address(), args)?; 68 | 69 | tracing::info!("Calling entrypoint with args: {:?}", args); 70 | let res = instance.call_typed_and_get_result::( 71 | self.context.data(), 72 | "pvq", 73 | (module.memory_map().aux_data_address(), args.len() as u32), 74 | )?; 75 | 76 | let res_size = (res >> 32) as u32; 77 | let res_ptr = (res & 0xffffffff) as u32; 78 | 79 | let result = instance.read_memory(res_ptr, res_size)?; 80 | 81 | Ok(result) 82 | })(); 83 | 84 | if gas_limit.is_some() { 85 | (result, Some(instance.gas())) 86 | } else { 87 | (result, None) 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /pvq-executor/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(not(feature = "std"), no_std)] 2 | 3 | extern crate alloc; 4 | 5 | pub use alloc::vec::Vec; 6 | pub use polkavm::{Caller, Config, Engine, Linker, Module, ProgramBlob}; 7 | 8 | mod context; 9 | mod error; 10 | mod executor; 11 | 12 | pub use context::PvqExecutorContext; 13 | pub use error::PvqExecutorError; 14 | pub use executor::PvqExecutor; 15 | -------------------------------------------------------------------------------- /pvq-extension-core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pvq-extension-core" 3 | description = "Core extension for PVQ" 4 | authors.workspace = true 5 | edition.workspace = true 6 | license.workspace = true 7 | repository.workspace = true 8 | version.workspace = true 9 | 10 | [dependencies] 11 | parity-scale-codec = { workspace = true } 12 | pvq-extension = { workspace = true } 13 | scale-info = { workspace = true } 14 | 15 | [features] 16 | default = ["std"] 17 | std = ["parity-scale-codec/std", "scale-info/std", "pvq-extension/std"] 18 | -------------------------------------------------------------------------------- /pvq-extension-core/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(not(feature = "std"), no_std)] 2 | 3 | use pvq_extension::extension_decl; 4 | 5 | #[extension_decl] 6 | pub mod extension { 7 | #[extension_decl::extension] 8 | pub trait ExtensionCore { 9 | type ExtensionId; 10 | fn has_extension(id: Self::ExtensionId) -> bool; 11 | // crypto functions 12 | // fn blake2_64(data: Vec) -> [u8; 8]; 13 | // fn blake2_128(data: Vec) -> [u8; 16]; 14 | // fn blake2_256(data: Vec) -> [u8; 32]; 15 | // fn twox_64(data: Vec) -> [u8; 8]; 16 | // fn read_storage(key: Vec) -> Option>; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /pvq-extension-fungibles/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pvq-extension-fungibles" 3 | description = "Fungibles extension for PVQ" 4 | authors.workspace = true 5 | edition.workspace = true 6 | license.workspace = true 7 | repository.workspace = true 8 | version.workspace = true 9 | 10 | [dependencies] 11 | parity-scale-codec = { workspace = true } 12 | pvq-extension = { workspace = true } 13 | scale-info = { workspace = true } 14 | 15 | [features] 16 | default = ["std"] 17 | std = ["parity-scale-codec/std", "scale-info/std", "pvq-extension/std"] 18 | -------------------------------------------------------------------------------- /pvq-extension-fungibles/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(not(feature = "std"), no_std)] 2 | use pvq_extension::extension_decl; 3 | 4 | #[extension_decl] 5 | pub mod extension { 6 | #[extension_decl::extension] 7 | pub trait ExtensionFungibles { 8 | type AssetId; 9 | type Balance; 10 | type AccountId; 11 | fn total_supply(asset: Self::AssetId) -> Self::Balance; 12 | fn balance(asset: Self::AssetId, who: Self::AccountId) -> Self::Balance; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /pvq-extension-swap/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pvq-extension-swap" 3 | description = "Swap extension for PVQ" 4 | authors.workspace = true 5 | edition.workspace = true 6 | license.workspace = true 7 | repository.workspace = true 8 | version.workspace = true 9 | 10 | [dependencies] 11 | parity-scale-codec = { workspace = true } 12 | pvq-extension = { workspace = true } 13 | scale-info = { workspace = true } 14 | 15 | [features] 16 | default = ["std"] 17 | std = ["parity-scale-codec/std", "scale-info/std", "pvq-extension/std"] 18 | -------------------------------------------------------------------------------- /pvq-extension-swap/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(not(feature = "std"), no_std)] 2 | extern crate alloc; 3 | 4 | use pvq_extension::extension_decl; 5 | 6 | #[extension_decl] 7 | pub mod extension { 8 | use alloc::vec::Vec; 9 | 10 | #[extension_decl::extension] 11 | pub trait ExtensionSwap { 12 | type AssetId; 13 | type Balance; 14 | 15 | fn quote_price_tokens_for_exact_tokens( 16 | asset1: Self::AssetId, 17 | asset2: Self::AssetId, 18 | amount: Self::Balance, 19 | include_fee: bool, 20 | ) -> Option; 21 | 22 | fn quote_price_exact_tokens_for_tokens( 23 | asset1: Self::AssetId, 24 | asset2: Self::AssetId, 25 | amount: Self::Balance, 26 | include_fee: bool, 27 | ) -> Option; 28 | 29 | fn get_liquidity_pool(asset1: Self::AssetId, asset2: Self::AssetId) -> Option<(Self::Balance, Self::Balance)>; 30 | 31 | #[allow(clippy::type_complexity)] 32 | fn list_pools() -> Vec<(Self::AssetId, Self::AssetId, Self::Balance, Self::Balance)>; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /pvq-extension/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pvq-extension" 3 | description = "Extension system for PVQ" 4 | authors.workspace = true 5 | edition.workspace = true 6 | license.workspace = true 7 | repository.workspace = true 8 | version.workspace = true 9 | 10 | [dependencies] 11 | polkavm = { workspace = true } 12 | pvq-executor = { workspace = true } 13 | fortuples = { workspace = true } 14 | tracing = { workspace = true } 15 | pvq-extension-procedural = { path = "procedural" } 16 | parity-scale-codec = { workspace = true } 17 | scale-info = { workspace = true } 18 | pvq-primitives = { workspace = true } 19 | serde = { workspace = true } 20 | 21 | [dev-dependencies] 22 | tracing-subscriber = { workspace = true } 23 | 24 | [features] 25 | default = ["std"] 26 | std = [ 27 | "polkavm/std", 28 | "pvq-executor/std", 29 | "pvq-primitives/std", 30 | "parity-scale-codec/std", 31 | "scale-info/std", 32 | "tracing/std", 33 | "serde/std", 34 | ] 35 | -------------------------------------------------------------------------------- /pvq-extension/procedural/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pvq-extension-procedural" 3 | authors.workspace = true 4 | edition.workspace = true 5 | repository.workspace = true 6 | license.workspace = true 7 | version.workspace = true 8 | 9 | [lib] 10 | proc-macro = true 11 | 12 | [dependencies] 13 | quote = { workspace = true } 14 | syn = { workspace = true } 15 | proc-macro2 = { workspace = true } 16 | proc-macro-crate = { workspace = true } 17 | twox-hash = "1.6.3" 18 | 19 | [dev-dependencies] 20 | pvq-extension = { workspace = true } 21 | scale-info = { workspace = true } 22 | parity-scale-codec = { workspace = true } 23 | trybuild = { workspace = true } 24 | -------------------------------------------------------------------------------- /pvq-extension/procedural/examples/proc_main/extension_decl.rs: -------------------------------------------------------------------------------- 1 | use pvq_extension_procedural::extension_decl; 2 | 3 | #[extension_decl] 4 | pub mod extension_without_associated_type { 5 | #[extension_decl::extension] 6 | pub trait ExtensionWithoutAssociatedType { 7 | fn test_fn(); 8 | } 9 | } 10 | 11 | #[extension_decl] 12 | pub mod extension_core { 13 | #[extension_decl::extension] 14 | pub trait ExtensionCore { 15 | type ExtensionId; 16 | fn has_extension(id: Self::ExtensionId) -> bool; 17 | } 18 | } 19 | 20 | #[extension_decl] 21 | pub mod extension_fungibles { 22 | #[extension_decl::extension] 23 | pub trait ExtensionFungibles { 24 | type AssetId; 25 | type AccountId; 26 | type Balance; 27 | fn total_supply(asset: Self::AssetId) -> Self::Balance; 28 | fn balance(asset: Self::AssetId, who: Self::AccountId) -> Self::Balance; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /pvq-extension/procedural/examples/proc_main/extension_impl.rs: -------------------------------------------------------------------------------- 1 | use pvq_extension_procedural::extensions_impl; 2 | 3 | #[extensions_impl] 4 | mod extensions_impl { 5 | use crate::extension_decl::{extension_core, extension_fungibles}; 6 | 7 | #[extensions_impl::impl_struct] 8 | pub struct ExtensionsImpl; 9 | 10 | #[extensions_impl::extension] 11 | impl extension_core::ExtensionCore for ExtensionsImpl { 12 | type ExtensionId = u64; 13 | fn has_extension(id: u64) -> bool { 14 | matches!(id, 0 | 1) 15 | } 16 | } 17 | 18 | #[extensions_impl::extension] 19 | impl extension_fungibles::ExtensionFungibles for ExtensionsImpl { 20 | type AssetId = u32; 21 | type AccountId = [u8; 32]; 22 | type Balance = u64; 23 | fn total_supply(asset: u32) -> u64 { 24 | 200 25 | } 26 | fn balance(asset: u32, who: [u8; 32]) -> u64 { 27 | 100 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /pvq-extension/procedural/examples/proc_main/main.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod extension_decl; 2 | pub(crate) mod extension_impl; 3 | fn main() {} 4 | -------------------------------------------------------------------------------- /pvq-extension/procedural/src/extension_decl/expand/extension.rs: -------------------------------------------------------------------------------- 1 | use super::helper; 2 | use crate::extension_decl::parse::extension::ExtensionFunction; 3 | use crate::extension_decl::Def; 4 | use proc_macro2::TokenStream as TokenStream2; 5 | use quote::quote; 6 | 7 | /// Replace Self::SomeType with Impl::SomeType 8 | fn replace_self_to_impl(ty: &syn::Type) -> Box { 9 | let ty_str = quote!(#ty).to_string(); 10 | 11 | let modified_ty_str = ty_str.replace("Self", "Impl"); 12 | 13 | let modified_ty = 14 | syn::parse_str(&modified_ty_str).expect("The replace with Impl::SomeType should not break the syntax"); 15 | 16 | Box::new(modified_ty) 17 | } 18 | 19 | pub fn expand_extension(def: &mut Def) -> TokenStream2 { 20 | let pvq_extension = &def.pvq_extension; 21 | let parity_scale_codec = &def.parity_scale_codec; 22 | // Set the trait name based on module_name 23 | let trait_ident = &def.extension.name; 24 | 25 | // Add super trait ExtensionId and ExtensionMetadata to the trait's where clause 26 | // helper::add_super_trait(&mut item_trait)?; 27 | 28 | // Generate the functions enum definition 29 | let functions_enum = expand_functions_enum(parity_scale_codec, trait_ident, &def.extension.functions); 30 | 31 | // Generate the dispatchable implementation 32 | let functions_impl_dispatchable = 33 | impl_dispatchable_for_functions(pvq_extension, parity_scale_codec, trait_ident, &def.extension.functions); 34 | 35 | // Generate the extension ID implementation 36 | let extension_id_expanded = expand_extension_id(pvq_extension, trait_ident, &def.extension.functions); 37 | 38 | // let extension_runtime_metadata = crate::runtime_metadata::generate_decl_metadata(&item_trait, view_fns.has_config)?; 39 | 40 | // Combine all the generated code 41 | let expanded = quote! { 42 | #functions_enum 43 | #functions_impl_dispatchable 44 | #extension_id_expanded 45 | // #extension_runtime_metadata 46 | }; 47 | 48 | expanded 49 | } 50 | 51 | fn expand_functions_enum( 52 | parity_scale_codec: &syn::Path, 53 | trait_ident: &syn::Ident, 54 | functions: &[ExtensionFunction], 55 | ) -> syn::ItemEnum { 56 | let mut variants = syn::punctuated::Punctuated::::new(); 57 | 58 | for function in functions { 59 | let name = &function.name; 60 | let mut inputs = syn::punctuated::Punctuated::::new(); 61 | 62 | for (name, ty) in &function.inputs { 63 | let ty = replace_self_to_impl(ty); 64 | inputs.push(syn::parse_quote! { 65 | #name: #ty 66 | }); 67 | } 68 | 69 | variants.push(syn::parse_quote! { 70 | #name { 71 | #inputs 72 | } 73 | }); 74 | } 75 | 76 | // Add phantom data 77 | variants.push(syn::parse_quote!( 78 | #[doc(hidden)] 79 | __marker(core::marker::PhantomData) 80 | )); 81 | syn::parse_quote!( 82 | #[derive(#parity_scale_codec::Encode, #parity_scale_codec::Decode)] 83 | #[allow(non_camel_case_types)] 84 | pub enum Functions { 85 | #variants 86 | } 87 | ) 88 | } 89 | 90 | fn impl_dispatchable_for_functions( 91 | pvq_extension: &syn::Path, 92 | parity_scale_codec: &syn::Path, 93 | trait_ident: &syn::Ident, 94 | functions: &[ExtensionFunction], 95 | ) -> syn::ItemImpl { 96 | let mut pats = Vec::::new(); 97 | 98 | for function in functions { 99 | let name = &function.name; 100 | let mut inputs = syn::punctuated::Punctuated::::new(); 101 | 102 | for (ident, _ty) in &function.inputs { 103 | inputs.push(ident.clone()); 104 | } 105 | 106 | pats.push(syn::parse_quote! { 107 | Self::#name { 108 | #inputs 109 | } 110 | }); 111 | } 112 | 113 | let mut method_calls = Vec::::new(); 114 | 115 | for function in functions { 116 | let name = &function.name; 117 | let mut inputs = syn::punctuated::Punctuated::::new(); 118 | 119 | for (ident, _ty) in &function.inputs { 120 | inputs.push(ident.clone()); 121 | } 122 | 123 | method_calls.push(syn::parse_quote! { 124 | Impl::#name(#inputs) 125 | }); 126 | } 127 | 128 | syn::parse_quote! { 129 | impl #pvq_extension::Dispatchable for Functions { 130 | fn dispatch(self) -> Result, #pvq_extension::DispatchError> { 131 | match self { 132 | #( #pats => Ok(#parity_scale_codec::Encode::encode(&#method_calls)),)* 133 | Self::__marker(_) => Err(#pvq_extension::DispatchError::PhantomData), 134 | } 135 | } 136 | } 137 | } 138 | } 139 | 140 | fn expand_extension_id( 141 | pvq_extension: &syn::Path, 142 | trait_ident: &syn::Ident, 143 | functions: &[ExtensionFunction], 144 | ) -> TokenStream2 { 145 | let extension_id = helper::calculate_hash(trait_ident, functions); 146 | quote::quote! { 147 | impl #pvq_extension::ExtensionId for Functions { 148 | const EXTENSION_ID: #pvq_extension::ExtensionIdTy = #extension_id; 149 | } 150 | pub fn extension_id() -> #pvq_extension::ExtensionIdTy { 151 | #extension_id 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /pvq-extension/procedural/src/extension_decl/expand/helper.rs: -------------------------------------------------------------------------------- 1 | use crate::extension_decl::parse::extension::ExtensionFunction; 2 | use std::hash::{Hash, Hasher}; 3 | use twox_hash::XxHash64; 4 | 5 | // Calculate hash for extension ID 6 | pub fn calculate_hash(trait_ident: &syn::Ident, functions: &[ExtensionFunction]) -> u64 { 7 | let mut hasher = XxHash64::default(); 8 | // reduce the chance of hash collision 9 | "pvq-ext$".hash(&mut hasher); 10 | trait_ident.hash(&mut hasher); 11 | for function in functions { 12 | // reduce the chance of hash collision 13 | "@".hash(&mut hasher); 14 | function.name.hash(&mut hasher); 15 | } 16 | hasher.finish() 17 | } 18 | -------------------------------------------------------------------------------- /pvq-extension/procedural/src/extension_decl/expand/metadata.rs: -------------------------------------------------------------------------------- 1 | use crate::extension_decl::Def; 2 | use proc_macro2::TokenStream as TokenStream2; 3 | use quote::quote; 4 | use syn::visit_mut::VisitMut; 5 | 6 | /// Generate the runtime metadata of the provided extension trait. 7 | pub fn expand_metadata(def: &Def) -> TokenStream2 { 8 | let pvq_extension = &def.pvq_extension; 9 | let scale_info = &def.scale_info; 10 | 11 | let mut functions = Vec::new(); 12 | 13 | let mut replacer = AssociatedTypeReplacer; 14 | 15 | for function in &def.extension.functions { 16 | let mut inputs = Vec::new(); 17 | for (name, ty) in &function.inputs { 18 | let name = name.to_string(); 19 | let mut ty = ty.clone(); 20 | 21 | // Replace Self::AssociatedType with Impl::AssociatedType 22 | replacer.visit_type_mut(&mut ty); 23 | 24 | inputs.push(quote!( 25 | #pvq_extension::metadata::FunctionParamMetadata { 26 | name: #name, 27 | ty: #scale_info::meta_type::<#ty>(), 28 | } 29 | )); 30 | } 31 | 32 | let output = match &function.output { 33 | syn::ReturnType::Default => quote!(#scale_info::meta_type::<()>()), 34 | syn::ReturnType::Type(_, ty) => { 35 | let mut ty = ty.clone(); 36 | // Replace Self::AssociatedType with Impl::AssociatedType 37 | replacer.visit_type_mut(&mut ty); 38 | quote!(#scale_info::meta_type::<#ty>()) 39 | } 40 | }; 41 | 42 | let function_name = function.name.to_string(); 43 | 44 | functions.push(quote!( 45 | #pvq_extension::metadata::FunctionMetadata { 46 | name: #function_name, 47 | inputs: #scale_info::prelude::vec![ #( #inputs, )* ], 48 | output: #output, 49 | } 50 | )); 51 | } 52 | 53 | let trait_ident = &def.extension.name; 54 | let trait_name = trait_ident.to_string(); 55 | if def.extension.types.is_empty() { 56 | quote!( 57 | pub fn metadata () -> #pvq_extension::metadata::ExtensionMetadata { 58 | #pvq_extension::metadata::ExtensionMetadata { 59 | name: #trait_name, 60 | functions: #scale_info::prelude::vec![ #( #functions, )* ], 61 | } 62 | } 63 | ) 64 | } else { 65 | let impl_generics = quote!(Impl: #trait_ident); 66 | quote!( 67 | pub fn metadata <#impl_generics> () -> #pvq_extension::metadata::ExtensionMetadata { 68 | #pvq_extension::metadata::ExtensionMetadata { 69 | name: #trait_name, 70 | functions: #scale_info::prelude::vec![ #( #functions, )* ], 71 | } 72 | } 73 | ) 74 | } 75 | } 76 | 77 | // Replace `Self` with `Impl` in the type path 78 | struct AssociatedTypeReplacer; 79 | impl syn::visit_mut::VisitMut for AssociatedTypeReplacer { 80 | fn visit_ident_mut(&mut self, ident: &mut syn::Ident) { 81 | if ident == "Self" { 82 | *ident = syn::Ident::new("Impl", ident.span()); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /pvq-extension/procedural/src/extension_decl/expand/mod.rs: -------------------------------------------------------------------------------- 1 | mod extension; 2 | mod helper; 3 | mod metadata; 4 | 5 | use crate::extension_decl::Def; 6 | use proc_macro2::TokenStream as TokenStream2; 7 | use quote::ToTokens; 8 | 9 | pub fn expand(mut def: Def) -> TokenStream2 { 10 | let extension_expanded = extension::expand_extension(&mut def); 11 | let metadata_expanded = metadata::expand_metadata(&def); 12 | let new_items = quote::quote! { 13 | #extension_expanded 14 | #metadata_expanded 15 | }; 16 | 17 | def.item 18 | .content 19 | .as_mut() 20 | .expect("This is checked by parsing") 21 | .1 22 | .push(syn::Item::Verbatim(new_items)); 23 | def.item.into_token_stream() 24 | } 25 | -------------------------------------------------------------------------------- /pvq-extension/procedural/src/extension_decl/mod.rs: -------------------------------------------------------------------------------- 1 | mod expand; 2 | pub(crate) mod parse; 3 | pub use parse::Def; 4 | use proc_macro::TokenStream; 5 | use proc_macro2::TokenStream as TokenStream2; 6 | use syn::spanned::Spanned; 7 | 8 | mod keyword { 9 | syn::custom_keyword!(dev_mode); 10 | } 11 | 12 | pub fn extension_decl(attr: TokenStream, item: TokenStream) -> TokenStream { 13 | if !attr.is_empty() { 14 | let msg = "Invalid #[extension_decl] macro call: unexpected attribute. Macro call must be bare, such as `#[extension_decl]`."; 15 | let span = TokenStream2::from(attr).span(); 16 | return syn::Error::new(span, msg).to_compile_error().into(); 17 | } 18 | 19 | let item = syn::parse_macro_input!(item as syn::ItemMod); 20 | match parse::Def::try_from(item) { 21 | Ok(def) => expand::expand(def).into(), 22 | Err(e) => e.to_compile_error().into(), 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /pvq-extension/procedural/src/extension_decl/parse/extension.rs: -------------------------------------------------------------------------------- 1 | use super::helper; 2 | use crate::utils::generate_crate_access; 3 | use std::collections::HashMap; 4 | use syn::spanned::Spanned; 5 | 6 | /// List of additional token to be used for parsing 7 | mod keyword { 8 | syn::custom_keyword!(extension_decl); 9 | syn::custom_keyword!(fn_index); 10 | } 11 | 12 | /// Definition of a ViewFns trait 13 | /// #[extension_decl::extension] 14 | /// pub trait ExtensionCore { 15 | /// type ExtensionId: Codec; 16 | /// #[extension_decl::fn_index(expr)] 17 | /// fn has_extension(id: Self::ExtensionId) -> bool; 18 | /// } 19 | pub struct Extension { 20 | /// The name of the trait 21 | pub name: syn::Ident, 22 | /// The associated type of the trait 23 | pub types: Vec, 24 | /// Information on functions 25 | pub functions: Vec, 26 | } 27 | 28 | #[derive(Debug)] 29 | pub struct ExtensionType { 30 | #[allow(dead_code)] 31 | pub name: syn::Ident, 32 | } 33 | 34 | /// Definition of a function variant 35 | pub struct ExtensionFunction { 36 | /// Function name 37 | pub name: syn::Ident, 38 | /// Information on inputs: `(name, type)` 39 | pub inputs: Vec<(syn::Ident, Box)>, 40 | /// The return type of the function 41 | pub output: syn::ReturnType, 42 | } 43 | 44 | /// Attributes for functions 45 | pub enum FunctionAttr { 46 | /// Parse for #[extension::fn_index(expr)] 47 | FnIndex(u8), 48 | } 49 | 50 | impl syn::parse::Parse for FunctionAttr { 51 | fn parse(input: syn::parse::ParseStream) -> syn::Result { 52 | input.parse::()?; 53 | let content; 54 | syn::bracketed!(content in input); 55 | content.parse::()?; 56 | content.parse::()?; 57 | 58 | let lookahead = content.lookahead1(); 59 | if lookahead.peek(keyword::fn_index) { 60 | content.parse::()?; 61 | let fn_index_content; 62 | syn::parenthesized!(fn_index_content in content); 63 | let index = fn_index_content.parse::()?; 64 | if !index.suffix().is_empty() { 65 | let msg = "Number literal must not have a suffix"; 66 | return Err(syn::Error::new(index.span(), msg)); 67 | } 68 | Ok(Self::FnIndex(index.base10_parse()?)) 69 | } else { 70 | Err(lookahead.error()) 71 | } 72 | } 73 | } 74 | 75 | impl Extension { 76 | pub fn try_from(item: &mut syn::Item) -> syn::Result { 77 | let parity_scale_codec = generate_crate_access("parity-scale-codec")?; 78 | let scale_info = generate_crate_access("scale-info")?; 79 | let syn::Item::Trait(item) = item else { 80 | let msg = "Invalid extension_decl::extension, expected trait definition"; 81 | return Err(syn::Error::new(item.span(), msg)); 82 | }; 83 | if !matches!(item.vis, syn::Visibility::Public(_)) { 84 | let msg = "Invalid extension_decl::extension, expected public trait definition"; 85 | return Err(syn::Error::new(item.span(), msg)); 86 | } 87 | 88 | if !item.generics.params.is_empty() { 89 | let msg = "Invalid extension_decl::extension, expected no generics"; 90 | return Err(syn::Error::new(item.generics.params[0].span(), msg)); 91 | } 92 | 93 | let mut types = vec![]; 94 | let mut functions = vec![]; 95 | let mut indices = HashMap::new(); 96 | let mut last_index: Option = None; 97 | for item in &mut item.items { 98 | match item { 99 | syn::TraitItem::Type(item_type) => { 100 | // Add `Codec + TypeInfo + 'static` bounds if not present 101 | if item_type 102 | .bounds 103 | .iter() 104 | .all(|bound| bound != &syn::parse_quote!(#parity_scale_codec::Codec)) 105 | { 106 | item_type.bounds.push(syn::parse_quote!(#parity_scale_codec::Codec)); 107 | } 108 | if item_type 109 | .bounds 110 | .iter() 111 | .all(|bound| bound != &syn::parse_quote!(#scale_info::TypeInfo)) 112 | { 113 | item_type.bounds.push(syn::parse_quote!(#scale_info::TypeInfo)); 114 | } 115 | if item_type 116 | .bounds 117 | .iter() 118 | .all(|bound| bound != &syn::parse_quote!('static)) 119 | { 120 | item_type.bounds.push(syn::parse_quote!('static)); 121 | } 122 | types.push(ExtensionType { 123 | name: item_type.ident.clone(), 124 | }); 125 | } 126 | syn::TraitItem::Fn(function) => { 127 | let mut function_index_attrs = vec![]; 128 | for attr in helper::take_item_extension_decl_attrs(&mut function.attrs)?.into_iter() { 129 | match attr { 130 | FunctionAttr::FnIndex(_) => { 131 | function_index_attrs.push(attr); 132 | } 133 | } 134 | } 135 | 136 | if function_index_attrs.len() > 1 { 137 | let msg = "Invalid extension_decl::extension, too many fn_index attributes given"; 138 | return Err(syn::Error::new(function.sig.span(), msg)); 139 | } 140 | 141 | let function_index = function_index_attrs.pop().map(|attr| match attr { 142 | FunctionAttr::FnIndex(index) => index, 143 | }); 144 | 145 | let final_index = match function_index { 146 | Some(i) => i, 147 | None => last_index.map_or(Some(0), |idx| idx.checked_add(1)).ok_or_else(|| { 148 | let msg = "Function index doesn't fit into u8, index is 256"; 149 | syn::Error::new(function.sig.span(), msg) 150 | })?, 151 | }; 152 | last_index = Some(final_index); 153 | 154 | if let Some(used_fn) = indices.insert(final_index, function.sig.ident.clone()) { 155 | let error_msg = format!( 156 | "Invalid extension_decl::extension; Both functions {} and {} are at index {}", 157 | used_fn, function.sig.ident, final_index 158 | ); 159 | let mut err = syn::Error::new(used_fn.span(), &error_msg); 160 | err.combine(syn::Error::new(function.sig.ident.span(), &error_msg)); 161 | return Err(err); 162 | } 163 | 164 | let mut inputs = vec![]; 165 | for input in function.sig.inputs.iter() { 166 | let input = if let syn::FnArg::Typed(input) = input { 167 | input 168 | } else { 169 | let msg = "Invalid extension_decl::extension, every input argument should be typed instead of receiver(self)"; 170 | return Err(syn::Error::new(input.span(), msg)); 171 | }; 172 | let input_ident = if let syn::Pat::Ident(pat) = &*input.pat { 173 | pat.ident.clone() 174 | } else { 175 | let msg = "Invalid extension_decl::extension, input argument must be ident"; 176 | return Err(syn::Error::new(input.pat.span(), msg)); 177 | }; 178 | inputs.push((input_ident, input.ty.clone())) 179 | } 180 | 181 | functions.push(ExtensionFunction { 182 | name: function.sig.ident.clone(), 183 | inputs, 184 | output: function.sig.output.clone(), 185 | }); 186 | } 187 | _ => {} 188 | } 189 | } 190 | 191 | if functions.is_empty() { 192 | let msg = "Invalid extension_decl::extension, expected at least one function"; 193 | return Err(syn::Error::new(item.span(), msg)); 194 | } 195 | 196 | Ok(Self { 197 | name: item.ident.clone(), 198 | types, 199 | functions, 200 | }) 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /pvq-extension/procedural/src/extension_decl/parse/helper.rs: -------------------------------------------------------------------------------- 1 | use quote::ToTokens; 2 | pub trait MutItemAttrs { 3 | fn mut_item_attrs(&mut self) -> Option<&mut Vec>; 4 | } 5 | 6 | /// Take the first extension_decl attribute (e.g. attribute like `#[extension_decl..]`) and decode it to `Attr` 7 | pub(crate) fn take_first_item_extension_decl_attr(item: &mut impl MutItemAttrs) -> syn::Result> 8 | where 9 | Attr: syn::parse::Parse, 10 | { 11 | let Some(attrs) = item.mut_item_attrs() else { 12 | return Ok(None); 13 | }; 14 | 15 | let Some(index) = attrs.iter().position(|attr| { 16 | attr.path() 17 | .segments 18 | .first() 19 | .is_some_and(|segment| segment.ident == "extension_decl") 20 | }) else { 21 | return Ok(None); 22 | }; 23 | 24 | let extension_attr = attrs.remove(index); 25 | Ok(Some(syn::parse2(extension_attr.into_token_stream())?)) 26 | } 27 | 28 | /// Take all the extension_decl attributes (e.g. attribute like `#[extension_decl..]`) and decode them to `Attr` 29 | pub(crate) fn take_item_extension_decl_attrs(item: &mut impl MutItemAttrs) -> syn::Result> 30 | where 31 | Attr: syn::parse::Parse, 32 | { 33 | let mut extension_attrs = Vec::new(); 34 | 35 | while let Some(attr) = take_first_item_extension_decl_attr(item)? { 36 | extension_attrs.push(attr) 37 | } 38 | 39 | Ok(extension_attrs) 40 | } 41 | 42 | impl MutItemAttrs for syn::Item { 43 | fn mut_item_attrs(&mut self) -> Option<&mut Vec> { 44 | match self { 45 | Self::Const(item) => Some(item.attrs.as_mut()), 46 | Self::Enum(item) => Some(item.attrs.as_mut()), 47 | Self::ExternCrate(item) => Some(item.attrs.as_mut()), 48 | Self::Fn(item) => Some(item.attrs.as_mut()), 49 | Self::ForeignMod(item) => Some(item.attrs.as_mut()), 50 | Self::Impl(item) => Some(item.attrs.as_mut()), 51 | Self::Macro(item) => Some(item.attrs.as_mut()), 52 | Self::Mod(item) => Some(item.attrs.as_mut()), 53 | Self::Static(item) => Some(item.attrs.as_mut()), 54 | Self::Struct(item) => Some(item.attrs.as_mut()), 55 | Self::Trait(item) => Some(item.attrs.as_mut()), 56 | Self::TraitAlias(item) => Some(item.attrs.as_mut()), 57 | Self::Type(item) => Some(item.attrs.as_mut()), 58 | Self::Union(item) => Some(item.attrs.as_mut()), 59 | Self::Use(item) => Some(item.attrs.as_mut()), 60 | _ => None, 61 | } 62 | } 63 | } 64 | 65 | impl MutItemAttrs for syn::TraitItem { 66 | fn mut_item_attrs(&mut self) -> Option<&mut Vec> { 67 | match self { 68 | Self::Const(item) => Some(item.attrs.as_mut()), 69 | Self::Fn(item) => Some(item.attrs.as_mut()), 70 | Self::Type(item) => Some(item.attrs.as_mut()), 71 | Self::Macro(item) => Some(item.attrs.as_mut()), 72 | _ => None, 73 | } 74 | } 75 | } 76 | 77 | impl MutItemAttrs for Vec { 78 | fn mut_item_attrs(&mut self) -> Option<&mut Vec> { 79 | Some(self) 80 | } 81 | } 82 | 83 | impl MutItemAttrs for syn::ItemMod { 84 | fn mut_item_attrs(&mut self) -> Option<&mut Vec> { 85 | Some(&mut self.attrs) 86 | } 87 | } 88 | 89 | impl MutItemAttrs for syn::ImplItemFn { 90 | fn mut_item_attrs(&mut self) -> Option<&mut Vec> { 91 | Some(&mut self.attrs) 92 | } 93 | } 94 | 95 | impl MutItemAttrs for syn::ItemType { 96 | fn mut_item_attrs(&mut self) -> Option<&mut Vec> { 97 | Some(&mut self.attrs) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /pvq-extension/procedural/src/extension_decl/parse/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod extension; 2 | mod helper; 3 | 4 | use crate::utils::generate_crate_access; 5 | use syn::spanned::Spanned; 6 | 7 | mod keyword { 8 | syn::custom_keyword!(extension_decl); 9 | syn::custom_keyword!(extension); 10 | } 11 | pub struct Def { 12 | pub item: syn::ItemMod, 13 | pub extension: extension::Extension, 14 | pub pvq_extension: syn::Path, 15 | pub scale_info: syn::Path, 16 | pub parity_scale_codec: syn::Path, 17 | } 18 | 19 | impl Def { 20 | pub fn try_from(mut item: syn::ItemMod) -> syn::Result { 21 | let pvq_extension = generate_crate_access("pvq-extension")?; 22 | let scale_info = generate_crate_access("scale-info")?; 23 | let parity_scale_codec = generate_crate_access("parity-scale-codec")?; 24 | let mod_span = item.span(); 25 | // Check if the module is public 26 | if !matches!(item.vis, syn::Visibility::Public(_)) { 27 | return Err(syn::Error::new( 28 | mod_span, 29 | "Invalid #[extension_decl] definition, expected public module.", 30 | )); 31 | } 32 | let items = &mut item 33 | .content 34 | .as_mut() 35 | .ok_or_else(|| { 36 | let msg = "Invalid #[extension_decl] definition, expected mod to be inline."; 37 | syn::Error::new(mod_span, msg) 38 | })? 39 | .1; 40 | let mut extension = None; 41 | for item in items.iter_mut() { 42 | let extension_attr: Option = helper::take_first_item_extension_decl_attr(item)?; 43 | 44 | match extension_attr { 45 | Some(ExtensionDeclAttr::Extension(_)) if extension.is_none() => { 46 | extension = Some(extension::Extension::try_from(item)?); 47 | } 48 | Some(attr) => { 49 | let msg = "Invalid duplicated attribute"; 50 | return Err(syn::Error::new(attr.span(), msg)); 51 | } 52 | None => (), 53 | } 54 | } 55 | 56 | Ok(Self { 57 | item, 58 | extension: extension.ok_or_else(|| syn::Error::new(mod_span, "Missing `#[extension_decl::extension]`"))?, 59 | pvq_extension, 60 | scale_info, 61 | parity_scale_codec, 62 | }) 63 | } 64 | } 65 | 66 | /// Parse attributes for item in extension module 67 | /// syntax must be `extension_decl::` (e.g. `#[extension_decl::extension]`) 68 | enum ExtensionDeclAttr { 69 | Extension(proc_macro2::Span), 70 | } 71 | 72 | impl syn::parse::Parse for ExtensionDeclAttr { 73 | fn parse(input: syn::parse::ParseStream) -> syn::Result { 74 | input.parse::()?; 75 | let content; 76 | syn::bracketed!(content in input); 77 | content.parse::()?; 78 | content.parse::()?; 79 | 80 | let lookahead = content.lookahead1(); 81 | if lookahead.peek(keyword::extension) { 82 | let span = content.parse::()?.span(); 83 | Ok(Self::Extension(span)) 84 | } else { 85 | Err(lookahead.error()) 86 | } 87 | } 88 | } 89 | 90 | impl ExtensionDeclAttr { 91 | fn span(&self) -> proc_macro2::Span { 92 | match self { 93 | Self::Extension(span) => *span, 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /pvq-extension/procedural/src/extensions_impl/expand/metadata.rs: -------------------------------------------------------------------------------- 1 | use crate::extensions_impl::Def; 2 | use proc_macro2::TokenStream as TokenStream2; 3 | use quote::quote; 4 | 5 | /// generate the `metadata` function in the #[extensions_impl] module 6 | pub fn expand_metadata(def: &Def) -> TokenStream2 { 7 | let pvq_extension = &def.pvq_extension; 8 | let scale_info = &def.scale_info; 9 | let mut extension_id_call_list = Vec::new(); 10 | let mut extension_metadata_call_list = Vec::new(); 11 | 12 | for impl_ in &def.extension_impls { 13 | let mut trait_path = impl_.trait_path.clone(); 14 | trait_path.segments.pop(); 15 | 16 | // Replace trait_path with a call to the metadata function with the impl struct as generic parameter 17 | let impl_struct_ident = &def.impl_struct.ident; 18 | 19 | let extension_id_call = quote!( 20 | #trait_path extension_id() 21 | ); 22 | // Create a method call expression instead of a path 23 | let method_call = quote!( 24 | #trait_path metadata::<#impl_struct_ident>() 25 | ); 26 | 27 | extension_id_call_list.push(extension_id_call); 28 | extension_metadata_call_list.push(method_call); 29 | } 30 | 31 | let metadata = quote! { 32 | pub fn metadata() -> #pvq_extension::metadata::Metadata { 33 | #pvq_extension::metadata::Metadata::new( 34 | #scale_info::prelude::collections::BTreeMap::from([ #( (#extension_id_call_list, #extension_metadata_call_list), )* ]), 35 | ) 36 | } 37 | }; 38 | metadata 39 | } 40 | -------------------------------------------------------------------------------- /pvq-extension/procedural/src/extensions_impl/expand/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::extensions_impl::Def; 2 | use proc_macro2::TokenStream as TokenStream2; 3 | use quote::ToTokens; 4 | mod metadata; 5 | 6 | fn expand_extensions_tuple(def: &Def) -> TokenStream2 { 7 | let mut extensions = Vec::new(); 8 | let impl_struct_ident = &def.impl_struct.ident; 9 | for impl_ in &def.extension_impls { 10 | let mut trait_path = impl_.trait_path.clone(); 11 | 12 | // Replace the last segment of the trait path with Functions 13 | if let Some(segment) = trait_path.segments.last_mut() { 14 | *segment = syn::parse_quote!(Functions<#impl_struct_ident>); 15 | } 16 | 17 | extensions.push(trait_path); 18 | } 19 | 20 | quote::quote! { 21 | pub type Extensions = ( 22 | #(#extensions),* 23 | ); 24 | } 25 | } 26 | 27 | pub fn expand(mut def: Def) -> TokenStream2 { 28 | let extensions_tuple_expanded = expand_extensions_tuple(&def); 29 | let metadata_expanded = metadata::expand_metadata(&def); 30 | let new_items = quote::quote! { 31 | #extensions_tuple_expanded 32 | #metadata_expanded 33 | }; 34 | 35 | def.item 36 | .content 37 | .as_mut() 38 | .expect("This is checked by parsing") 39 | .1 40 | .push(syn::Item::Verbatim(new_items)); 41 | def.item.into_token_stream() 42 | } 43 | -------------------------------------------------------------------------------- /pvq-extension/procedural/src/extensions_impl/mod.rs: -------------------------------------------------------------------------------- 1 | mod expand; 2 | mod parse; 3 | pub use parse::Def; 4 | use proc_macro::TokenStream; 5 | use proc_macro2::TokenStream as TokenStream2; 6 | use syn::spanned::Spanned; 7 | 8 | pub fn extensions_impl(attr: TokenStream, item: TokenStream) -> TokenStream { 9 | if !attr.is_empty() { 10 | let msg = "Invalid extensions_impl macro call: unexpected attribute. Macro call must be bare, such as `#[extensions_impl]`."; 11 | let span = TokenStream2::from(attr).span(); 12 | return syn::Error::new(span, msg).to_compile_error().into(); 13 | } 14 | 15 | let item = syn::parse_macro_input!(item as syn::ItemMod); 16 | match parse::Def::try_from(item) { 17 | Ok(def) => expand::expand(def).into(), 18 | Err(e) => e.to_compile_error().into(), 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /pvq-extension/procedural/src/extensions_impl/parse/extension.rs: -------------------------------------------------------------------------------- 1 | use syn::spanned::Spanned; 2 | use syn::Error; 3 | pub struct ExtensionImpl { 4 | pub trait_path: syn::Path, 5 | } 6 | 7 | impl ExtensionImpl { 8 | pub fn try_from(item: &mut syn::Item) -> syn::Result { 9 | let syn::Item::Impl(item) = item else { 10 | let msg = "Invalid extensions_impl::extension, expected impl definition"; 11 | return Err(syn::Error::new(item.span(), msg)); 12 | }; 13 | // Check if's a impl trait and the trait is qualified 14 | let path = item 15 | .trait_ 16 | .as_ref() 17 | .map(|v| &v.1) 18 | .ok_or_else(|| Error::new(item.span(), "Only implementation of traits are supported!")) 19 | .and_then(|p| { 20 | // The implemented trait has to be referenced with a fully qualified path, 21 | if p.segments.len() > 1 { 22 | Ok(p) 23 | } else { 24 | Err(Error::new( 25 | p.span(), 26 | "The implemented trait has to be referenced with a fully qualified path, \ 27 | e.g. `impl pvq_extension_core::ExtensionCore for ExtensionsImpl`.", 28 | )) 29 | } 30 | })?; 31 | Ok(Self { 32 | trait_path: path.clone(), 33 | }) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /pvq-extension/procedural/src/extensions_impl/parse/helper.rs: -------------------------------------------------------------------------------- 1 | use quote::ToTokens; 2 | pub trait MutItemAttrs { 3 | fn mut_item_attrs(&mut self) -> Option<&mut Vec>; 4 | } 5 | 6 | /// Take the first extensions_impl attribute (e.g. attribute like `#[extensions_impl..]`) and decode it to `Attr` 7 | pub(crate) fn take_first_item_extensions_impl_attr(item: &mut impl MutItemAttrs) -> syn::Result> 8 | where 9 | Attr: syn::parse::Parse, 10 | { 11 | let Some(attrs) = item.mut_item_attrs() else { 12 | return Ok(None); 13 | }; 14 | 15 | let Some(index) = attrs.iter().position(|attr| { 16 | attr.path() 17 | .segments 18 | .first() 19 | .is_some_and(|segment| segment.ident == "extensions_impl") 20 | }) else { 21 | return Ok(None); 22 | }; 23 | 24 | let extension_attr = attrs.remove(index); 25 | Ok(Some(syn::parse2(extension_attr.into_token_stream())?)) 26 | } 27 | 28 | /// Take all the extensions_impl attributes (e.g. attribute like `#[extensions_impl..]`) and decode them to `Attr` 29 | #[allow(dead_code)] 30 | pub(crate) fn take_item_extensions_impl_attrs(item: &mut impl MutItemAttrs) -> syn::Result> 31 | where 32 | Attr: syn::parse::Parse, 33 | { 34 | let mut extension_attrs = Vec::new(); 35 | 36 | while let Some(attr) = take_first_item_extensions_impl_attr(item)? { 37 | extension_attrs.push(attr) 38 | } 39 | 40 | Ok(extension_attrs) 41 | } 42 | 43 | impl MutItemAttrs for syn::Item { 44 | fn mut_item_attrs(&mut self) -> Option<&mut Vec> { 45 | match self { 46 | Self::Const(item) => Some(item.attrs.as_mut()), 47 | Self::Enum(item) => Some(item.attrs.as_mut()), 48 | Self::ExternCrate(item) => Some(item.attrs.as_mut()), 49 | Self::Fn(item) => Some(item.attrs.as_mut()), 50 | Self::ForeignMod(item) => Some(item.attrs.as_mut()), 51 | Self::Impl(item) => Some(item.attrs.as_mut()), 52 | Self::Macro(item) => Some(item.attrs.as_mut()), 53 | Self::Mod(item) => Some(item.attrs.as_mut()), 54 | Self::Static(item) => Some(item.attrs.as_mut()), 55 | Self::Struct(item) => Some(item.attrs.as_mut()), 56 | Self::Trait(item) => Some(item.attrs.as_mut()), 57 | Self::TraitAlias(item) => Some(item.attrs.as_mut()), 58 | Self::Type(item) => Some(item.attrs.as_mut()), 59 | Self::Union(item) => Some(item.attrs.as_mut()), 60 | Self::Use(item) => Some(item.attrs.as_mut()), 61 | _ => None, 62 | } 63 | } 64 | } 65 | 66 | impl MutItemAttrs for syn::TraitItem { 67 | fn mut_item_attrs(&mut self) -> Option<&mut Vec> { 68 | match self { 69 | Self::Const(item) => Some(item.attrs.as_mut()), 70 | Self::Fn(item) => Some(item.attrs.as_mut()), 71 | Self::Type(item) => Some(item.attrs.as_mut()), 72 | Self::Macro(item) => Some(item.attrs.as_mut()), 73 | _ => None, 74 | } 75 | } 76 | } 77 | 78 | impl MutItemAttrs for Vec { 79 | fn mut_item_attrs(&mut self) -> Option<&mut Vec> { 80 | Some(self) 81 | } 82 | } 83 | 84 | impl MutItemAttrs for syn::ItemMod { 85 | fn mut_item_attrs(&mut self) -> Option<&mut Vec> { 86 | Some(&mut self.attrs) 87 | } 88 | } 89 | 90 | impl MutItemAttrs for syn::ImplItemFn { 91 | fn mut_item_attrs(&mut self) -> Option<&mut Vec> { 92 | Some(&mut self.attrs) 93 | } 94 | } 95 | 96 | impl MutItemAttrs for syn::ItemType { 97 | fn mut_item_attrs(&mut self) -> Option<&mut Vec> { 98 | Some(&mut self.attrs) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /pvq-extension/procedural/src/extensions_impl/parse/impl_struct.rs: -------------------------------------------------------------------------------- 1 | use syn::spanned::Spanned; 2 | pub struct ImplStruct { 3 | pub ident: syn::Ident, 4 | } 5 | impl ImplStruct { 6 | pub fn try_from(item: &mut syn::Item) -> syn::Result { 7 | let syn::Item::Struct(item) = item else { 8 | let msg = "Invalid extensions_impl::impl_struct, expected struct definition"; 9 | return Err(syn::Error::new(item.span(), msg)); 10 | }; 11 | 12 | Ok(Self { 13 | ident: item.ident.clone(), 14 | }) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /pvq-extension/procedural/src/extensions_impl/parse/mod.rs: -------------------------------------------------------------------------------- 1 | mod extension; 2 | mod helper; 3 | mod impl_struct; 4 | use crate::utils::generate_crate_access; 5 | use syn::spanned::Spanned; 6 | 7 | mod keyword { 8 | syn::custom_keyword!(extensions_impl); 9 | syn::custom_keyword!(impl_struct); 10 | syn::custom_keyword!(extension); 11 | } 12 | 13 | pub struct Def { 14 | pub item: syn::ItemMod, 15 | pub impl_struct: impl_struct::ImplStruct, 16 | pub extension_impls: Vec, 17 | pub pvq_extension: syn::Path, 18 | pub scale_info: syn::Path, 19 | } 20 | 21 | impl Def { 22 | pub fn try_from(mut item: syn::ItemMod) -> syn::Result { 23 | let pvq_extension = generate_crate_access("pvq-extension")?; 24 | let scale_info = generate_crate_access("scale-info")?; 25 | let item_span = item.span(); 26 | let items = &mut item 27 | .content 28 | .as_mut() 29 | .ok_or_else(|| { 30 | let msg = "Invalid extensions_impl definition, expected mod to be inline."; 31 | syn::Error::new(item_span, msg) 32 | })? 33 | .1; 34 | let mut impl_struct = None; 35 | let mut extension_impls = Vec::new(); 36 | for item in items.iter_mut() { 37 | let extensions_impl_attr: Option = helper::take_first_item_extensions_impl_attr(item)?; 38 | match extensions_impl_attr { 39 | Some(ExtensionsImplAttr::ImplStruct(_)) if impl_struct.is_none() => { 40 | impl_struct = Some(impl_struct::ImplStruct::try_from(item)?); 41 | } 42 | Some(ExtensionsImplAttr::Extension(_)) => { 43 | extension_impls.push(extension::ExtensionImpl::try_from(item)?); 44 | } 45 | Some(attr) => { 46 | let msg = "Invalid duplicated attribute"; 47 | return Err(syn::Error::new(attr.span(), msg)); 48 | } 49 | None => (), 50 | } 51 | } 52 | 53 | if extension_impls.is_empty() { 54 | let msg = "At least one `#[extensions_impl::extension]` is required"; 55 | return Err(syn::Error::new(item_span, msg)); 56 | } 57 | Ok(Self { 58 | item, 59 | impl_struct: impl_struct 60 | .ok_or_else(|| syn::Error::new(item_span, "Missing `#[extensions_impl::impl_struct]`"))?, 61 | extension_impls, 62 | pvq_extension, 63 | scale_info, 64 | }) 65 | } 66 | } 67 | 68 | enum ExtensionsImplAttr { 69 | ImplStruct(proc_macro2::Span), 70 | Extension(proc_macro2::Span), 71 | } 72 | 73 | impl syn::parse::Parse for ExtensionsImplAttr { 74 | fn parse(input: syn::parse::ParseStream) -> syn::Result { 75 | input.parse::()?; 76 | let content; 77 | syn::bracketed!(content in input); 78 | content.parse::()?; 79 | content.parse::()?; 80 | 81 | let lookahead = content.lookahead1(); 82 | if lookahead.peek(keyword::impl_struct) { 83 | let span = content.parse::()?.span(); 84 | Ok(Self::ImplStruct(span)) 85 | } else if lookahead.peek(keyword::extension) { 86 | let span = content.parse::()?.span(); 87 | Ok(Self::Extension(span)) 88 | } else { 89 | Err(lookahead.error()) 90 | } 91 | } 92 | } 93 | 94 | impl ExtensionsImplAttr { 95 | fn span(&self) -> proc_macro2::Span { 96 | match self { 97 | Self::ImplStruct(span) => *span, 98 | Self::Extension(span) => *span, 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /pvq-extension/procedural/src/lib.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::TokenStream; 2 | mod extension_decl; 3 | mod extensions_impl; 4 | pub(crate) mod utils; 5 | 6 | #[proc_macro_attribute] 7 | pub fn extension_decl(attr: TokenStream, item: TokenStream) -> TokenStream { 8 | extension_decl::extension_decl(attr, item) 9 | } 10 | 11 | #[proc_macro_attribute] 12 | pub fn extensions_impl(attr: TokenStream, item: TokenStream) -> TokenStream { 13 | extensions_impl::extensions_impl(attr, item) 14 | } 15 | -------------------------------------------------------------------------------- /pvq-extension/procedural/src/utils.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::Span; 2 | use proc_macro_crate::{crate_name, FoundCrate}; 3 | pub fn generate_crate_access(def_crate: &str) -> syn::Result { 4 | let ident = match crate_name(def_crate) { 5 | Ok(FoundCrate::Itself) => { 6 | let name = def_crate.replace('-', "_"); 7 | Ok(syn::Ident::new(&name, Span::call_site())) 8 | } 9 | Ok(FoundCrate::Name(name)) => Ok(syn::Ident::new(&name, Span::call_site())), 10 | Err(e) => Err(syn::Error::new(Span::call_site(), e)), 11 | }?; 12 | Ok(syn::Path::from(ident)) 13 | } 14 | -------------------------------------------------------------------------------- /pvq-extension/procedural/tests/tests.rs: -------------------------------------------------------------------------------- 1 | #[test] 2 | fn test_macros() { 3 | let t = trybuild::TestCases::new(); 4 | // Test successful cases 5 | t.pass("tests/ui/extension_decl/pass/*.rs"); 6 | t.pass("tests/ui/extensions_impl/pass/*.rs"); 7 | 8 | // Test failing cases 9 | t.compile_fail("tests/ui/extension_decl/*.rs"); 10 | t.compile_fail("tests/ui/extensions_impl/*.rs"); 11 | } 12 | -------------------------------------------------------------------------------- /pvq-extension/procedural/tests/ui/extension_decl/not_on_mod.rs: -------------------------------------------------------------------------------- 1 | use pvq_extension_procedural::extension_decl; 2 | 3 | // This should fail because extension_decl can only be used on modules 4 | #[extension_decl] 5 | struct InvalidUsage { 6 | field: u32, 7 | } 8 | 9 | fn main() {} 10 | -------------------------------------------------------------------------------- /pvq-extension/procedural/tests/ui/extension_decl/not_on_mod.stderr: -------------------------------------------------------------------------------- 1 | error: expected `mod` 2 | --> tests/ui/extension_decl/not_on_mod.rs:5:1 3 | | 4 | 5 | struct InvalidUsage { 5 | | ^^^^^^ 6 | -------------------------------------------------------------------------------- /pvq-extension/procedural/tests/ui/extension_decl/pass/sucess.rs: -------------------------------------------------------------------------------- 1 | use pvq_extension_procedural::extension_decl; 2 | 3 | #[extension_decl] 4 | pub mod test_extension { 5 | #[extension_decl::extension] 6 | pub trait TestExtension { 7 | type Value; 8 | fn test_fn(value: Self::Value) -> bool; 9 | } 10 | } 11 | 12 | fn main() {} 13 | -------------------------------------------------------------------------------- /pvq-extension/procedural/tests/ui/extensions_impl/missing_impl_struct.rs: -------------------------------------------------------------------------------- 1 | use pvq_extension_procedural::{extension_decl, extensions_impl}; 2 | 3 | #[extension_decl] 4 | pub mod test_extension { 5 | #[extension_decl::extension] 6 | pub trait TestExtension { 7 | type Value; 8 | fn test_fn(value: Self::Value) -> bool; 9 | } 10 | } 11 | 12 | #[extensions_impl] 13 | mod test_impl { 14 | // Missing #[extensions_impl::impl_struct] attribute 15 | pub struct TestImpl; 16 | 17 | #[extensions_impl::extension] 18 | impl test_extension::TestExtension for TestImpl { 19 | type Value = u32; 20 | fn test_fn(value: u32) -> bool { 21 | value > 0 22 | } 23 | } 24 | } 25 | 26 | fn main() {} 27 | -------------------------------------------------------------------------------- /pvq-extension/procedural/tests/ui/extensions_impl/missing_impl_struct.stderr: -------------------------------------------------------------------------------- 1 | error: Missing `#[extensions_impl::impl_struct]` 2 | --> tests/ui/extensions_impl/missing_impl_struct.rs:13:1 3 | | 4 | 13 | / mod test_impl { 5 | 14 | | // Missing #[extensions_impl::impl_struct] attribute 6 | 15 | | pub struct TestImpl; 7 | ... | 8 | 23 | | } 9 | 24 | | } 10 | | |_^ 11 | -------------------------------------------------------------------------------- /pvq-extension/procedural/tests/ui/extensions_impl/pass/success.rs: -------------------------------------------------------------------------------- 1 | use pvq_extension_procedural::{extension_decl, extensions_impl}; 2 | 3 | #[extension_decl] 4 | pub mod test_extension { 5 | #[extension_decl::extension] 6 | pub trait TestExtension { 7 | type Value; 8 | fn test_fn(value: Self::Value) -> bool; 9 | } 10 | } 11 | 12 | #[extensions_impl] 13 | mod test_impl { 14 | use super::test_extension; 15 | #[extensions_impl::impl_struct] 16 | pub struct TestImpl; 17 | 18 | #[extensions_impl::extension] 19 | impl test_extension::TestExtension for TestImpl { 20 | type Value = u32; 21 | fn test_fn(value: u32) -> bool { 22 | value > 0 23 | } 24 | } 25 | } 26 | 27 | fn main() {} 28 | -------------------------------------------------------------------------------- /pvq-extension/src/calldata.rs: -------------------------------------------------------------------------------- 1 | use parity_scale_codec::Decode; 2 | use scale_info::prelude::vec::Vec; 3 | 4 | /// Type for extension IDs 5 | pub type ExtensionIdTy = u64; 6 | 7 | /// Trait for identifying extensions 8 | pub trait ExtensionId { 9 | const EXTENSION_ID: ExtensionIdTy; 10 | } 11 | 12 | /// Trait for dispatching extension calls 13 | pub trait Dispatchable { 14 | fn dispatch(self) -> Result, DispatchError>; 15 | } 16 | 17 | /// Error type for dispatch operations 18 | #[derive(Debug)] 19 | pub enum DispatchError { 20 | PhantomData, 21 | } 22 | 23 | /// Trait for extension call data 24 | /// 25 | /// This trait combines several traits that are required for extension call data: 26 | /// - `Dispatchable`: Allows dispatching calls to the extension functions 27 | /// - `ExtensionId`: Identifies the extension 28 | /// - `Decode`: Allows decoding the call data 29 | pub trait CallData: Dispatchable + ExtensionId + Decode {} 30 | impl CallData for T where T: Dispatchable + ExtensionId + Decode {} 31 | -------------------------------------------------------------------------------- /pvq-extension/src/context.rs: -------------------------------------------------------------------------------- 1 | use pvq_executor::{Caller, Linker, PvqExecutorContext}; 2 | use scale_info::prelude::marker::PhantomData; 3 | 4 | use crate::{ 5 | error::ExtensionError, 6 | perm_controller::{InvokeSource, PermissionController}, 7 | CallDataTuple, 8 | }; 9 | 10 | /// Execution context for extensions 11 | /// 12 | /// This struct provides the context for executing extensions. 13 | /// It includes the invoke source and user data. 14 | pub struct Context { 15 | /// The source of the invocation 16 | invoke_source: InvokeSource, 17 | /// User data for the context 18 | user_data: (), 19 | /// Marker for the generic parameters 20 | _marker: PhantomData<(C, P)>, 21 | } 22 | 23 | impl Context { 24 | /// Create a new context 25 | /// 26 | /// # Arguments 27 | /// 28 | /// * `invoke_source` - The source of the invocation 29 | pub fn new(invoke_source: InvokeSource) -> Self { 30 | Self { 31 | invoke_source, 32 | user_data: (), 33 | _marker: PhantomData, 34 | } 35 | } 36 | } 37 | 38 | impl PvqExecutorContext for Context { 39 | type UserData = (); 40 | type UserError = ExtensionError; 41 | 42 | fn data(&mut self) -> &mut Self::UserData { 43 | &mut self.user_data 44 | } 45 | 46 | fn register_host_functions(&mut self, linker: &mut Linker) { 47 | let invoke_source = self.invoke_source; 48 | 49 | // Register the host_call function 50 | linker 51 | .define_typed( 52 | "host_call", 53 | move |caller: Caller<'_, Self::UserData>, 54 | extension_id: u64, 55 | call_ptr: u32, 56 | call_len: u32| 57 | -> Result { 58 | tracing::info!( 59 | "(host call): extension_id: {}, call_ptr: {}, call_len: {}", 60 | extension_id, 61 | call_ptr, 62 | call_len 63 | ); 64 | // Read the call data from memory 65 | let call_bytes = caller.instance.read_memory(call_ptr, call_len)?; 66 | 67 | // Check permissions 68 | if !P::is_allowed(extension_id, &call_bytes, invoke_source) { 69 | return Err(ExtensionError::PermissionError); 70 | } 71 | 72 | // Dispatch the call 73 | let res_bytes = C::dispatch(extension_id, &call_bytes)?; 74 | tracing::debug!("(host call): dispatch result: {:?}", res_bytes); 75 | 76 | // Allocate memory for the response 77 | let res_bytes_len = res_bytes.len(); 78 | let res_ptr = caller 79 | .instance 80 | .sbrk(0) 81 | .map_err(|_| ExtensionError::MemoryAllocationError)? 82 | .ok_or(ExtensionError::MemoryAllocationError)?; 83 | caller 84 | .instance 85 | .sbrk(res_bytes_len as u32) 86 | .map_err(|_| ExtensionError::MemoryAllocationError)? 87 | .ok_or(ExtensionError::MemoryAllocationError)?; 88 | 89 | // Write the response to memory 90 | caller.instance.write_memory(res_ptr, &res_bytes)?; 91 | 92 | // Return the pointer and length 93 | Ok(((res_bytes_len as u64) << 32) | (res_ptr as u64)) 94 | }, 95 | ) 96 | .expect("Failed to register host_call function"); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /pvq-extension/src/error.rs: -------------------------------------------------------------------------------- 1 | // TODO: contain source error 2 | use crate::DispatchError; 3 | use parity_scale_codec::Error as CodecError; 4 | use scale_info::prelude::fmt; 5 | use scale_info::prelude::fmt::{Display, Formatter}; 6 | 7 | /// Errors that can occur when working with extensions 8 | // Typically will be used as a UserError 9 | #[derive(Debug)] 10 | pub enum ExtensionError { 11 | /// Permission denied for the requested operation 12 | PermissionError, 13 | 14 | /// Failed to allocate memory 15 | MemoryAllocationError, 16 | 17 | /// Error accessing memory 18 | MemoryAccessError(polkavm::MemoryAccessError), 19 | 20 | /// Error decoding data 21 | DecodeError(CodecError), 22 | 23 | /// Error dispatching a call 24 | DispatchError(DispatchError), 25 | 26 | /// The requested extension is not supported 27 | UnsupportedExtension, 28 | } 29 | 30 | impl Display for ExtensionError { 31 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 32 | match self { 33 | Self::PermissionError => write!(f, "Permission denied"), 34 | Self::MemoryAllocationError => write!(f, "Failed to allocate memory"), 35 | Self::MemoryAccessError(e) => write!(f, "Memory access error: {:?}", e), 36 | Self::DecodeError(e) => write!(f, "Decode error: {:?}", e), 37 | Self::DispatchError(e) => write!(f, "Dispatch error: {:?}", e), 38 | Self::UnsupportedExtension => write!(f, "Unsupported extension"), 39 | } 40 | } 41 | } 42 | 43 | #[cfg(feature = "std")] 44 | impl std::error::Error for ExtensionError {} 45 | 46 | impl From for ExtensionError { 47 | fn from(e: polkavm::MemoryAccessError) -> Self { 48 | Self::MemoryAccessError(e) 49 | } 50 | } 51 | 52 | impl From for ExtensionError { 53 | fn from(e: CodecError) -> Self { 54 | Self::DecodeError(e) 55 | } 56 | } 57 | 58 | impl From for ExtensionError { 59 | fn from(e: DispatchError) -> Self { 60 | Self::DispatchError(e) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /pvq-extension/src/executor.rs: -------------------------------------------------------------------------------- 1 | use pvq_executor::PvqExecutor; 2 | use pvq_primitives::{PvqError, PvqResult}; 3 | 4 | use crate::{ 5 | perm_controller::{InvokeSource, PermissionController}, 6 | CallDataTuple, Context, 7 | }; 8 | 9 | /// Executor for extensions 10 | /// 11 | /// This struct provides an executor for running extension code. 12 | /// It wraps a PvqExecutor with a Context for extensions. 13 | pub struct ExtensionsExecutor { 14 | /// The underlying PVQ executor 15 | executor: PvqExecutor>, 16 | } 17 | 18 | impl ExtensionsExecutor { 19 | /// Create a new extensions executor 20 | /// 21 | /// # Arguments 22 | /// 23 | /// * `source` - The source of the invocation 24 | pub fn new(source: InvokeSource) -> Self { 25 | let context = Context::::new(source); 26 | let executor = PvqExecutor::new(Default::default(), context); 27 | Self { executor } 28 | } 29 | 30 | /// 31 | /// # Arguments 32 | /// 33 | /// * `program` - The program data 34 | /// * `args` - The input data 35 | /// 36 | /// # Returns 37 | /// 38 | /// The result of the execution or an error 39 | pub fn execute(&mut self, program: &[u8], args: &[u8], gas_limit: Option) -> (PvqResult, Option) { 40 | let (result, gas_remaining) = self.executor.execute(program, args, gas_limit); 41 | tracing::info!("result: {:?}", result); 42 | (result.map_err(PvqError::from), gas_remaining) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /pvq-extension/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(not(feature = "std"), no_std)] 2 | //! # PVQ Extension System 3 | //! 4 | //! This crate provides an extension system for PVQ (PolkaVM Query). 5 | //! It allows defining and implementing extensions that can be called from PVQ queries. 6 | //! 7 | //! ## Overview 8 | //! 9 | //! The extension system consists of: 10 | //! 11 | //! - **Extension Definitions**: Traits that define the API of an extension 12 | //! - **Extension Implementations**: Implementations of extension traits 13 | //! - **Extension Executor**: A runtime for executing queries 14 | //! - **Permission Control**: Access control mechanisms to control access to extensions 15 | //! 16 | //! ## Usage 17 | //! 18 | //! Extensions are defined using the `#[extension_decl]` macro and implemented using the 19 | //! `#[extensions_impl]` macro. See the examples directory for usage examples. 20 | 21 | // Re-exports 22 | pub use pvq_extension_procedural::{extension_decl, extensions_impl}; 23 | 24 | // Module declarations 25 | mod calldata; 26 | mod context; 27 | mod error; 28 | mod executor; 29 | mod macros; 30 | pub mod metadata; 31 | mod perm_controller; 32 | 33 | // Public exports 34 | pub use calldata::{CallData, DispatchError, Dispatchable, ExtensionId, ExtensionIdTy}; 35 | pub use context::Context; 36 | pub use error::ExtensionError; 37 | pub use executor::ExtensionsExecutor; 38 | pub use macros::CallDataTuple; 39 | pub use metadata::{ExtensionImplMetadata, ExtensionMetadata}; 40 | pub use perm_controller::{InvokeSource, PermissionController}; 41 | -------------------------------------------------------------------------------- /pvq-extension/src/macros.rs: -------------------------------------------------------------------------------- 1 | use crate::{CallData, ExtensionError, ExtensionIdTy}; 2 | use fortuples::fortuples; 3 | use scale_info::prelude::vec::Vec; 4 | 5 | /// Trait for a tuple of extension call data types 6 | pub trait CallDataTuple { 7 | /// Dispatch a call to an extension 8 | /// 9 | /// # Arguments 10 | /// 11 | /// * `extension_id` - The ID of the extension to call 12 | /// * `data` - The encoded call data 13 | /// 14 | /// # Returns 15 | /// 16 | /// The encoded response data or an error 17 | fn dispatch(extension_id: ExtensionIdTy, data: &[u8]) -> Result, ExtensionError>; 18 | } 19 | 20 | impl CallDataTuple for Member0 21 | where 22 | Member0: CallData, 23 | { 24 | fn dispatch(extension_id: ExtensionIdTy, mut data: &[u8]) -> Result, ExtensionError> { 25 | if extension_id == Member0::EXTENSION_ID { 26 | return Member0::decode(&mut data) 27 | .map_err(ExtensionError::DecodeError)? 28 | .dispatch() 29 | .map_err(ExtensionError::DispatchError); 30 | } 31 | Err(ExtensionError::UnsupportedExtension) 32 | } 33 | } 34 | 35 | fortuples! { 36 | #[tuples::min_size(1)] 37 | impl CallDataTuple for #Tuple where #(#Member: CallData),*{ 38 | #[allow(unused_variables)] 39 | #[allow(unused_mut)] 40 | fn dispatch(extension_id: ExtensionIdTy, mut call: &[u8]) -> Result, ExtensionError> { 41 | #( 42 | if extension_id == #Member::EXTENSION_ID { 43 | return #Member::decode(&mut call).map_err(ExtensionError::DecodeError)?.dispatch().map_err(ExtensionError::DispatchError); 44 | } 45 | )* 46 | Err(ExtensionError::UnsupportedExtension) 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /pvq-extension/src/metadata.rs: -------------------------------------------------------------------------------- 1 | use crate::ExtensionIdTy; 2 | 3 | // This trait is for ExtensionImpl 4 | pub trait ExtensionImplMetadata { 5 | fn extension_metadata(extension_id: ExtensionIdTy) -> ExtensionMetadata; 6 | } 7 | 8 | use parity_scale_codec::Encode; 9 | use scale_info::{ 10 | form::{Form, MetaForm, PortableForm}, 11 | prelude::collections::BTreeMap, 12 | prelude::vec::Vec, 13 | IntoPortable, PortableRegistry, Registry, 14 | }; 15 | use serde::Serialize; 16 | /// Metadata of extensions 17 | #[derive(Clone, PartialEq, Eq, Encode, Debug, Serialize)] 18 | pub struct Metadata { 19 | pub types: PortableRegistry, 20 | pub extensions: BTreeMap>, 21 | } 22 | 23 | impl Metadata { 24 | pub fn new(extensions: BTreeMap) -> Self { 25 | let mut registry = Registry::new(); 26 | let extensions = extensions 27 | .into_iter() 28 | .map(|(id, metadata)| (id, metadata.into_portable(&mut registry))) 29 | .collect(); 30 | Self { 31 | types: registry.into(), 32 | extensions, 33 | } 34 | } 35 | } 36 | 37 | /// Metadata of an extension. 38 | #[derive(Clone, PartialEq, Eq, Encode, Debug)] 39 | pub struct ExtensionMetadata { 40 | pub name: T::String, 41 | pub functions: Vec>, 42 | } 43 | 44 | impl IntoPortable for ExtensionMetadata { 45 | type Output = ExtensionMetadata; 46 | 47 | fn into_portable(self, registry: &mut Registry) -> Self::Output { 48 | ExtensionMetadata { 49 | name: self.name.into_portable(registry), 50 | functions: registry.map_into_portable(self.functions), 51 | } 52 | } 53 | } 54 | 55 | impl Serialize for ExtensionMetadata { 56 | fn serialize(&self, serializer: S) -> Result 57 | where 58 | S: serde::Serializer, 59 | { 60 | use serde::ser::SerializeStruct; 61 | let mut state = serializer.serialize_struct("ExtensionMetadata", 2)?; 62 | state.serialize_field("name", &self.name)?; 63 | state.serialize_field("functions", &self.functions)?; 64 | state.end() 65 | } 66 | } 67 | 68 | /// Metadata of a runtime function. 69 | #[derive(Clone, PartialEq, Eq, Encode, Debug)] 70 | pub struct FunctionMetadata { 71 | /// Method name. 72 | pub name: T::String, 73 | /// Method parameters. 74 | pub inputs: Vec>, 75 | /// Method output. 76 | pub output: T::Type, 77 | } 78 | 79 | impl IntoPortable for FunctionMetadata { 80 | type Output = FunctionMetadata; 81 | 82 | fn into_portable(self, registry: &mut Registry) -> Self::Output { 83 | FunctionMetadata { 84 | name: self.name.into_portable(registry), 85 | inputs: registry.map_into_portable(self.inputs), 86 | output: registry.register_type(&self.output), 87 | } 88 | } 89 | } 90 | 91 | impl Serialize for FunctionMetadata { 92 | fn serialize(&self, serializer: S) -> Result 93 | where 94 | S: serde::Serializer, 95 | { 96 | use serde::ser::SerializeStruct; 97 | let mut state = serializer.serialize_struct("FunctionMetadata", 3)?; 98 | state.serialize_field("name", &self.name)?; 99 | state.serialize_field("inputs", &self.inputs)?; 100 | state.serialize_field("output", &self.output)?; 101 | state.end() 102 | } 103 | } 104 | 105 | /// Metadata of a runtime method parameter. 106 | #[derive(Clone, PartialEq, Eq, Encode, Debug)] 107 | pub struct FunctionParamMetadata { 108 | /// Parameter name. 109 | pub name: T::String, 110 | /// Parameter type. 111 | pub ty: T::Type, 112 | } 113 | 114 | impl IntoPortable for FunctionParamMetadata { 115 | type Output = FunctionParamMetadata; 116 | 117 | fn into_portable(self, registry: &mut Registry) -> Self::Output { 118 | FunctionParamMetadata { 119 | name: self.name.into_portable(registry), 120 | ty: registry.register_type(&self.ty), 121 | } 122 | } 123 | } 124 | 125 | impl Serialize for FunctionParamMetadata { 126 | fn serialize(&self, serializer: S) -> Result 127 | where 128 | S: serde::Serializer, 129 | { 130 | use serde::ser::SerializeStruct; 131 | let mut state = serializer.serialize_struct("FunctionParamMetadata", 2)?; 132 | state.serialize_field("name", &self.name)?; 133 | state.serialize_field("ty", &self.ty)?; 134 | state.end() 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /pvq-extension/src/perm_controller.rs: -------------------------------------------------------------------------------- 1 | use crate::ExtensionIdTy; 2 | 3 | /// Source of an extension invocation 4 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] 5 | pub enum InvokeSource { 6 | /// Invoked from a runtime API 7 | RuntimeAPI, 8 | 9 | /// Invoked from XCM (Cross-Consensus Message) 10 | XCM, 11 | 12 | /// Invoked from an extrinsic 13 | Extrinsic, 14 | 15 | /// Invoked from the runtime inside 16 | Runtime, 17 | } 18 | 19 | /// Controller for extension permissions 20 | /// 21 | /// This trait is used to control access to extensions based on the extension ID, 22 | /// call data, and invocation source. 23 | pub trait PermissionController { 24 | /// Check if a call to an extension is allowed 25 | /// 26 | /// # Arguments 27 | /// 28 | /// * `extension_id` - The ID of the extension 29 | /// * `call` - The encoded call data 30 | /// * `source` - The source of the invocation 31 | /// 32 | /// # Returns 33 | /// 34 | /// `true` if the call is allowed, `false` otherwise 35 | fn is_allowed(extension_id: ExtensionIdTy, call: &[u8], source: InvokeSource) -> bool; 36 | } 37 | 38 | /// Default permission controller that allows everything 39 | impl PermissionController for () { 40 | fn is_allowed(_extension_id: ExtensionIdTy, _call: &[u8], _source: InvokeSource) -> bool { 41 | true 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /pvq-primitives/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pvq-primitives" 3 | description = "Primitives for PVQ" 4 | authors.workspace = true 5 | edition.workspace = true 6 | repository.workspace = true 7 | license.workspace = true 8 | version.workspace = true 9 | 10 | [dependencies] 11 | parity-scale-codec = { workspace = true } 12 | scale-info = { workspace = true } 13 | 14 | [features] 15 | default = ["std"] 16 | std = ["parity-scale-codec/std", "scale-info/std"] 17 | -------------------------------------------------------------------------------- /pvq-primitives/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(not(feature = "std"), no_std)] 2 | 3 | extern crate alloc; 4 | 5 | use alloc::vec::Vec; 6 | use parity_scale_codec::{Decode, Encode}; 7 | use scale_info::TypeInfo; 8 | 9 | pub type PvqResult = Result; 10 | 11 | pub type PvqResponse = Vec; 12 | 13 | #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TypeInfo)] 14 | pub enum PvqError { 15 | FailedToDecode, 16 | InvalidPvqProgramFormat, 17 | QueryExceedsWeightLimit, 18 | Trap, 19 | MemoryAccessError, 20 | HostCallError, 21 | Other, 22 | } 23 | -------------------------------------------------------------------------------- /pvq-program-metadata-gen/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pvq-program-metadata-gen" 3 | description = "PVQ program metadata generation" 4 | authors.workspace = true 5 | edition.workspace = true 6 | license.workspace = true 7 | repository.workspace = true 8 | version.workspace = true 9 | 10 | [dependencies] 11 | quote = { workspace = true } 12 | syn = { workspace = true } 13 | proc-macro2 = { workspace = true } 14 | clap = { workspace = true } 15 | parity-scale-codec = { workspace = true } 16 | scale-info = { workspace = true } 17 | toml = { workspace = true } 18 | tempfile = { workspace = true } 19 | tracing = { workspace = true } 20 | tracing-subscriber = { workspace = true } 21 | -------------------------------------------------------------------------------- /pvq-program-metadata-gen/README.md: -------------------------------------------------------------------------------- 1 | # PVQ Program Metadata Generator 2 | 3 | A command-line tool for generating metadata for PVQ programs. This tool extracts metadata from your PVQ program source code, allowing the UI to know about your program's metadata. 4 | 5 | ## Installation 6 | 7 | You can install the tool globally using Cargo: 8 | 9 | ```bash 10 | cargo install --path /path/to/pvq-program-metadata-gen 11 | ``` 12 | 13 | ## Usage 14 | 15 | The basic usage is as follows: 16 | 17 | ```bash 18 | pvq-program-metadata-gen --crate-path /path/to/your/crate --output-dir /path/to/output/dir 19 | ``` 20 | 21 | ### Arguments 22 | 23 | - `--crate-path, -c`: Path to the crate directory containing a PVQ program 24 | - `--output-dir, -o`: Output directory for the metadata file, typically `METADATA_OUTPUT_DIR` environment variable read by `build.rs` 25 | 26 | ## Integration with Build Scripts 27 | 28 | You can integrate this tool into your crate's `build.rs` file: 29 | 30 | ```rust 31 | use std::env; 32 | use std::path::PathBuf; 33 | use std::process::Command; 34 | 35 | fn main() { 36 | // Tell Cargo to rerun this build script if the source file changes 37 | let current_dir = env::current_dir().expect("Failed to get current directory"); 38 | // Determine the output directory for the metadata 39 | let output_dir = PathBuf::from(env::var("METADATA_OUTPUT_DIR").expect("METADATA_OUTPUT_DIR is not set")); 40 | 41 | // Build and run the command 42 | let status = Command::new("pvq-program-metadata-gen") 43 | .arg("--crate-path") 44 | .arg(¤t_dir) 45 | .arg("--output-dir") 46 | .arg(&output_dir) 47 | .env("RUST_LOG", "info") 48 | .status() 49 | .expect("Failed to execute pvq-program-metadata-gen"); 50 | 51 | if !status.success() { 52 | panic!("Failed to generate program metadata"); 53 | } 54 | } 55 | 56 | ``` 57 | 58 | ## How It Works 59 | 60 | The tool: 61 | 62 | 1. Reads the source code of your PVQ program 63 | 2. Generates metadata generation code 64 | 3. Creates a temporary crate that store the metadata generation code 65 | 4. Compiles and runs the temporary crate using the same conditions as your original crate 66 | 67 | The metadata includes information about function names, parameter types, and return types, allowing the UI to know about your program's metadata. 68 | -------------------------------------------------------------------------------- /pvq-program-metadata-gen/src/bin/pvq-program-metadata-gen.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use std::fs; 3 | use std::path::PathBuf; 4 | use std::process::Command; 5 | use tracing::{debug, info}; 6 | 7 | #[derive(Parser, Debug)] 8 | #[command(author, version, about = "PVQ Program Metadata Generator")] 9 | struct Args { 10 | /// Path to the crate directory containing a PVQ program 11 | #[arg(short, long)] 12 | crate_path: PathBuf, 13 | 14 | #[arg(short, long)] 15 | output_dir: PathBuf, 16 | } 17 | 18 | fn main() { 19 | // Initialize tracing 20 | tracing_subscriber::fmt::init(); 21 | 22 | // Parse command line arguments 23 | let args = Args::parse(); 24 | 25 | // Logging arguments 26 | info!("Generating metadata for program at: {}", args.crate_path.display()); 27 | info!("Output dir: {}", args.output_dir.display()); 28 | 29 | // Create a temp crate for the metadata generation 30 | let temp_dir = tempfile::tempdir().expect("Failed to create temp directory"); 31 | let temp_crate_path = temp_dir.path(); 32 | fs::create_dir_all(temp_crate_path).expect("Failed to create `temp_crate` directory"); 33 | info!("Temp crate path: {}", temp_crate_path.display()); 34 | 35 | // Extract features section from the original manifest 36 | let original_manifest_content = 37 | std::fs::read_to_string(args.crate_path.join("Cargo.toml")).expect("Failed to read original Cargo.toml"); 38 | let optional_features = pvq_program_metadata_gen::extract_features(&original_manifest_content) 39 | .expect("Failed to extract features section from the original Cargo.toml"); 40 | debug!("Features section: {:?}", optional_features); 41 | 42 | // Create Cargo.toml with features from the original crate 43 | let manifest = pvq_program_metadata_gen::create_manifest(optional_features.as_ref()); 44 | debug!("Manifest: {}", manifest); 45 | std::fs::write(temp_crate_path.join("Cargo.toml"), manifest).expect("Failed to write Cargo.toml"); 46 | 47 | // Add active features to the cargo command 48 | let active_features = pvq_program_metadata_gen::get_active_features(optional_features.as_ref()) 49 | .expect("Failed to get active features"); 50 | debug!("Active features: {:?}", active_features); 51 | 52 | // Read the program source 53 | let source = fs::read_to_string(args.crate_path.join("src/main.rs")) 54 | .expect("Failed to read pvq program source file, expected `src/main.rs`"); 55 | 56 | let pkg_name = std::env::var("CARGO_PKG_NAME").expect("CARGO_PKG_NAME is not set"); 57 | 58 | // Generate the metadata generator source codes 59 | let metadata_gen_src = 60 | pvq_program_metadata_gen::metadata_gen_src(&source, &pkg_name, args.output_dir.to_string_lossy().as_ref()) 61 | .expect("Failed to generate metadata generator source code"); 62 | debug!("Metadata generator source code: {}", metadata_gen_src); 63 | 64 | // Create src directory and write main.rs 65 | fs::create_dir_all(temp_crate_path.join("src")).expect("Failed to create `temp_crate/src directory"); 66 | fs::write(temp_crate_path.join("src/main.rs"), metadata_gen_src.to_string()) 67 | .expect("Failed to write metadata generator source code"); 68 | 69 | // Compile and run the metadata generator in one step 70 | let mut cargo_cmd = Command::new("cargo"); 71 | cargo_cmd.current_dir(temp_crate_path).args(["run"]); 72 | for feature in active_features { 73 | cargo_cmd.arg("--features").arg(feature); 74 | } 75 | info!("Compiling and running metadata generator..."); 76 | let status = cargo_cmd.status().expect("Failed to run metadata generator"); 77 | if !status.success() { 78 | panic!("Failed to generate metadata"); 79 | } 80 | info!("Metadata generation successful!"); 81 | } 82 | -------------------------------------------------------------------------------- /pvq-program-metadata-gen/src/features.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | pub fn get_active_features(optional_features: Option<&toml::Table>) -> Result, String> { 3 | let features_env = std::env::vars() 4 | .filter(|(var, _)| var.starts_with("CARGO_FEATURE_")) 5 | .map(|(var, _)| var) 6 | .collect::>(); 7 | if features_env.is_empty() { 8 | Ok(vec![]) 9 | } else { 10 | let features = optional_features 11 | .as_ref() 12 | .ok_or_else(|| "Some features are set, but there is no features section in the manifest".to_string())?; 13 | Ok(features 14 | .keys() 15 | .filter(|feature| { 16 | features_env.contains(&format!("CARGO_FEATURE_{}", feature.to_uppercase().replace("-", "_"))) 17 | }) 18 | .map(|feature| feature.to_string()) 19 | .collect()) 20 | } 21 | } 22 | 23 | /// Extracts features from the original crate's Cargo.toml 24 | pub fn extract_features(original_manifest_content: &str) -> Result, String> { 25 | match toml::from_str::(original_manifest_content) { 26 | Ok(manifest) => { 27 | // Extract features section if it exists 28 | if let Some(features) = manifest.get("features") { 29 | if let toml::Value::Table(features) = features { 30 | Ok(Some(features.clone())) 31 | } else { 32 | Err("features section is not a table".to_string()) 33 | } 34 | } else { 35 | Ok(None) 36 | } 37 | } 38 | Err(e) => Err(e.to_string()), 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /pvq-program-metadata-gen/src/helper.rs: -------------------------------------------------------------------------------- 1 | pub trait MutItemAttrs { 2 | fn mut_item_attrs(&mut self) -> Option<&mut Vec>; 3 | } 4 | /// Take the first item attribute (e.g. attribute like `#[pvq..]`) and decode it to `Attr` 5 | pub(crate) fn take_first_program_attr(item: &mut impl MutItemAttrs) -> syn::Result> { 6 | let Some(attrs) = item.mut_item_attrs() else { 7 | return Ok(None); 8 | }; 9 | 10 | let Some(index) = attrs.iter().position(|attr| { 11 | attr.path() 12 | .segments 13 | .first() 14 | .is_some_and(|segment| segment.ident == "program") 15 | }) else { 16 | return Ok(None); 17 | }; 18 | 19 | let pvq_attr = attrs.remove(index); 20 | Ok(Some(pvq_attr)) 21 | } 22 | impl MutItemAttrs for syn::Item { 23 | fn mut_item_attrs(&mut self) -> Option<&mut Vec> { 24 | match self { 25 | Self::Const(item) => Some(item.attrs.as_mut()), 26 | Self::Enum(item) => Some(item.attrs.as_mut()), 27 | Self::ExternCrate(item) => Some(item.attrs.as_mut()), 28 | Self::Fn(item) => Some(item.attrs.as_mut()), 29 | Self::ForeignMod(item) => Some(item.attrs.as_mut()), 30 | Self::Impl(item) => Some(item.attrs.as_mut()), 31 | Self::Macro(item) => Some(item.attrs.as_mut()), 32 | Self::Mod(item) => Some(item.attrs.as_mut()), 33 | Self::Static(item) => Some(item.attrs.as_mut()), 34 | Self::Struct(item) => Some(item.attrs.as_mut()), 35 | Self::Trait(item) => Some(item.attrs.as_mut()), 36 | Self::TraitAlias(item) => Some(item.attrs.as_mut()), 37 | Self::Type(item) => Some(item.attrs.as_mut()), 38 | Self::Union(item) => Some(item.attrs.as_mut()), 39 | Self::Use(item) => Some(item.attrs.as_mut()), 40 | _ => None, 41 | } 42 | } 43 | } 44 | 45 | impl MutItemAttrs for syn::TraitItem { 46 | fn mut_item_attrs(&mut self) -> Option<&mut Vec> { 47 | match self { 48 | Self::Const(item) => Some(item.attrs.as_mut()), 49 | Self::Fn(item) => Some(item.attrs.as_mut()), 50 | Self::Type(item) => Some(item.attrs.as_mut()), 51 | Self::Macro(item) => Some(item.attrs.as_mut()), 52 | _ => None, 53 | } 54 | } 55 | } 56 | 57 | impl MutItemAttrs for Vec { 58 | fn mut_item_attrs(&mut self) -> Option<&mut Vec> { 59 | Some(self) 60 | } 61 | } 62 | 63 | impl MutItemAttrs for syn::ItemMod { 64 | fn mut_item_attrs(&mut self) -> Option<&mut Vec> { 65 | Some(&mut self.attrs) 66 | } 67 | } 68 | 69 | impl MutItemAttrs for syn::ImplItemFn { 70 | fn mut_item_attrs(&mut self) -> Option<&mut Vec> { 71 | Some(&mut self.attrs) 72 | } 73 | } 74 | 75 | impl MutItemAttrs for syn::ItemType { 76 | fn mut_item_attrs(&mut self) -> Option<&mut Vec> { 77 | Some(&mut self.attrs) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /pvq-program-metadata-gen/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub type ExtensionId = u64; 2 | pub type FnIndex = u8; 3 | mod features; 4 | mod helper; 5 | pub use features::{extract_features, get_active_features}; 6 | mod manifest; 7 | pub use manifest::create_manifest; 8 | mod metadata_gen; 9 | pub use metadata_gen::metadata_gen_src; 10 | -------------------------------------------------------------------------------- /pvq-program-metadata-gen/src/manifest.rs: -------------------------------------------------------------------------------- 1 | /// Creates a Cargo.toml file for the metadata generator 2 | pub fn create_manifest(features: Option<&toml::Table>) -> String { 3 | // Create a basic Cargo.toml for the temp crate 4 | format!( 5 | r#"[package] 6 | name = "metadata_gen" 7 | version = "0.1.0" 8 | edition = "2021" 9 | 10 | [dependencies] 11 | scale-info = {{ version = "2.0.0", features = ["derive","serde"] }} 12 | parity-scale-codec = {{ version = "3.0.0", features = ["derive"] }} 13 | serde = {{ version = "1", features = ["derive" ] }} 14 | serde_json = "1" 15 | cfg-if = "1.0" 16 | {0} 17 | "#, 18 | features.map_or_else(String::new, |features| format!( 19 | "\n[features]\n{}", 20 | toml::to_string(&features).expect("Should be checked in parsing") 21 | )) 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /pvq-program-metadata-gen/src/metadata_gen.rs: -------------------------------------------------------------------------------- 1 | use quote::{quote, ToTokens}; 2 | use syn::spanned::Spanned; 3 | 4 | type ExtensionId = u64; 5 | type FnIndex = u8; 6 | 7 | pub fn metadata_gen_src(source: &str, pkg_name: &str, output_dir: &str) -> syn::Result { 8 | // Parse the source code 9 | let mut syntax = syn::parse_file(source)?; 10 | 11 | // Find the program module 12 | // Find the index of the program module 13 | let program_mod_idx = syntax 14 | .items 15 | .iter() 16 | .position(|item| matches!(item, syn::Item::Mod(m) if m.attrs.iter().any(|attr|attr.path().segments.last().is_some_and(|last|last.ident == "program")))) 17 | .ok_or(syn::Error::new( 18 | proc_macro2::Span::call_site(), 19 | "No program module found", 20 | ))?; 21 | 22 | // Remove the program module from syntax.items 23 | let mut program_mod = match syntax.items.remove(program_mod_idx) { 24 | syn::Item::Mod(m) => m, 25 | _ => unreachable!("We already checked this is a module"), 26 | }; 27 | 28 | // Remove the program attr 29 | program_mod.attrs.clear(); 30 | let program_mod_items = &mut program_mod.content.as_mut().expect("This is checked before").1; 31 | 32 | // Find entrypoint and extension functions 33 | let mut entrypoint_metadata = None; 34 | let mut extension_fns_metadata = Vec::new(); 35 | 36 | for i in (0..program_mod_items.len()).rev() { 37 | let item = &mut program_mod_items[i]; 38 | if let Some(attr) = crate::helper::take_first_program_attr(item)? { 39 | if let Some(last_segment) = attr.path().segments.last() { 40 | if last_segment.ident == "extension_fn" { 41 | let mut extension_id = None; 42 | let mut fn_index = None; 43 | attr.parse_nested_meta(|meta| { 44 | if meta.path.is_ident("extension_id") { 45 | let value = meta.value()?; 46 | extension_id = Some(value.parse::()?.base10_parse::()?); 47 | } else if meta.path.is_ident("fn_index") { 48 | let value = meta.value()?; 49 | fn_index = Some(value.parse::()?.base10_parse::()?); 50 | } else { 51 | return Err(syn::Error::new( 52 | meta.path.span(), 53 | "Invalid attribute meta, expected `extension_id` or `fn_index`", 54 | )); 55 | } 56 | Ok(()) 57 | })?; 58 | let removed_item = program_mod_items.remove(i); 59 | if extension_id.is_none() || fn_index.is_none() { 60 | return Err(syn::Error::new( 61 | attr.span(), 62 | "Extension ID and function index are required", 63 | )); 64 | } 65 | let extension_id = 66 | extension_id.ok_or_else(|| syn::Error::new(attr.span(), "Extension ID is required"))?; 67 | let fn_index = 68 | fn_index.ok_or_else(|| syn::Error::new(attr.span(), "Function index is required"))?; 69 | let extension_fn_metadata = generate_extension_fn_metadata(removed_item, extension_id, fn_index)?; 70 | extension_fns_metadata.push(extension_fn_metadata); 71 | } else if last_segment.ident == "entrypoint" { 72 | if entrypoint_metadata.is_some() { 73 | return Err(syn::Error::new(attr.span(), "Multiple entrypoint functions found")); 74 | } 75 | let removed_item = program_mod_items.remove(i); 76 | entrypoint_metadata = Some(generate_entrypoint_metadata(removed_item)?); 77 | } else { 78 | return Err(syn::Error::new( 79 | attr.span(), 80 | "Invalid attribute, expected `#[program::extension_fn]` or `#[program::entrypoint]`", 81 | )); 82 | } 83 | } 84 | } 85 | } 86 | 87 | let entrypoint_metadata = entrypoint_metadata 88 | .ok_or_else(|| syn::Error::new(proc_macro2::Span::call_site(), "No entrypoint function found"))?; 89 | 90 | let metadata_defs = metadata_defs(); 91 | let import_packages = import_packages(); 92 | 93 | let new_items = quote! { 94 | #(#program_mod_items)* 95 | #import_packages 96 | #metadata_defs 97 | fn main() { 98 | let extension_fns = vec![ #( #extension_fns_metadata, )* ]; 99 | let entrypoint = #entrypoint_metadata; 100 | let metadata = Metadata::new(extension_fns, entrypoint); 101 | // Serialize to both formats 102 | let encoded = parity_scale_codec::Encode::encode(&metadata); 103 | let json = serde_json::to_string(&metadata).expect("Failed to serialize metadata to JSON"); 104 | 105 | let bin_path = Path::new(#output_dir).join(format!("{}-metadata.bin", #pkg_name)); 106 | let json_path = Path::new(#output_dir).join(format!("{}-metadata.json", #pkg_name)); 107 | 108 | // Write the binary format 109 | std::fs::write(bin_path, &encoded).expect("Failed to write binary metadata"); 110 | 111 | // Write the JSON format 112 | std::fs::write(json_path, json).expect("Failed to write JSON metadata"); 113 | } 114 | }; 115 | 116 | // Remove #![no_main] and #![no_std] attributes if present 117 | syntax.attrs.retain(|attr| { 118 | if let Some(segment) = attr.path().segments.last() { 119 | let ident = &segment.ident; 120 | !(ident == "no_main" || ident == "no_std") 121 | } else { 122 | true 123 | } 124 | }); 125 | 126 | syntax.items.push(syn::Item::Verbatim(new_items)); 127 | 128 | Ok(syntax.into_token_stream()) 129 | } 130 | 131 | fn generate_extension_fn_metadata( 132 | f: syn::Item, 133 | extension_id: ExtensionId, 134 | fn_index: FnIndex, 135 | ) -> syn::Result { 136 | if let syn::Item::Fn(f) = f { 137 | let fn_name = f.sig.ident.to_string(); 138 | let mut inputs = Vec::new(); 139 | for input in &f.sig.inputs { 140 | if let syn::FnArg::Typed(syn::PatType { pat, ty, .. }) = input { 141 | if let syn::Pat::Ident(pat_ident) = &**pat { 142 | let name = pat_ident.ident.to_string(); 143 | inputs.push(quote!( 144 | FunctionParamMetadata { 145 | name: #name, 146 | ty: scale_info::meta_type::<#ty>(), 147 | } 148 | )); 149 | } else { 150 | return Err(syn::Error::new(input.span(), "Expected a typed argument")); 151 | } 152 | } else { 153 | return Err(syn::Error::new(input.span(), "Expected a typed argument")); 154 | } 155 | } 156 | let output = match &f.sig.output { 157 | syn::ReturnType::Default => quote!(scale_info::meta_type::<()>()), 158 | syn::ReturnType::Type(_, ty) => { 159 | quote!(scale_info::meta_type::<#ty>()) 160 | } 161 | }; 162 | Ok(quote! { 163 | (#extension_id, #fn_index, FunctionMetadata { 164 | name: #fn_name, 165 | inputs: vec![#(#inputs,)*], 166 | output: #output 167 | }) 168 | }) 169 | } else { 170 | Err(syn::Error::new(f.span(), "Expected a function")) 171 | } 172 | } 173 | fn generate_entrypoint_metadata(f: syn::Item) -> syn::Result { 174 | if let syn::Item::Fn(f) = f { 175 | let name = f.sig.ident.to_string(); 176 | let mut inputs = Vec::new(); 177 | for input in &f.sig.inputs { 178 | if let syn::FnArg::Typed(syn::PatType { pat, ty, .. }) = input { 179 | if let syn::Pat::Ident(pat_ident) = &**pat { 180 | let name = pat_ident.ident.to_string(); 181 | inputs.push(quote!( 182 | FunctionParamMetadata { 183 | name: #name, 184 | ty: scale_info::meta_type::<#ty>(), 185 | } 186 | )); 187 | } else { 188 | return Err(syn::Error::new(input.span(), "Expected a typed argument")); 189 | } 190 | } else { 191 | return Err(syn::Error::new(input.span(), "Expected a typed argument")); 192 | } 193 | } 194 | let output = match &f.sig.output { 195 | syn::ReturnType::Default => quote!(scale_info::meta_type::<()>()), 196 | syn::ReturnType::Type(_, ty) => { 197 | quote!(scale_info::meta_type::<#ty>()) 198 | } 199 | }; 200 | Ok(quote! { 201 | FunctionMetadata { 202 | name: #name, 203 | inputs: vec![#(#inputs,)*], 204 | output: #output 205 | } 206 | }) 207 | } else { 208 | Err(syn::Error::new(f.span(), "Expected a function")) 209 | } 210 | } 211 | 212 | fn import_packages() -> proc_macro2::TokenStream { 213 | quote! { 214 | extern crate alloc; 215 | use std::path::Path; 216 | use serde::Serialize; 217 | use parity_scale_codec::Encode; 218 | use scale_info::{ 219 | form::{Form, MetaForm, PortableForm}, 220 | prelude::vec::Vec, 221 | IntoPortable, PortableRegistry, Registry, 222 | }; 223 | } 224 | } 225 | fn metadata_defs() -> proc_macro2::TokenStream { 226 | quote! { 227 | type ExtensionId = u64; 228 | type FnIndex = u8; 229 | /// Metadata of extensions 230 | #[derive(Clone, PartialEq, Eq, Encode, Debug, Serialize)] 231 | pub struct Metadata { 232 | pub types: PortableRegistry, 233 | pub extension_fns: Vec<(ExtensionId, FnIndex, FunctionMetadata)>, 234 | pub entrypoint: FunctionMetadata, 235 | } 236 | 237 | impl Metadata { 238 | pub fn new(extension_fns: Vec<(ExtensionId, FnIndex, FunctionMetadata)>, entrypoint: FunctionMetadata) -> Self { 239 | let mut registry = Registry::new(); 240 | let extension_fns = extension_fns 241 | .into_iter() 242 | .map(|(id, index, metadata)| (id, index, metadata.into_portable(&mut registry))) 243 | .collect(); 244 | let entrypoint = entrypoint.into_portable(&mut registry); 245 | Self { 246 | types: registry.into(), 247 | extension_fns, 248 | entrypoint, 249 | } 250 | } 251 | } 252 | 253 | #[derive(Clone, PartialEq, Eq, Encode, Debug, Serialize)] 254 | pub struct FunctionMetadata { 255 | pub name: T::String, 256 | pub inputs: Vec>, 257 | pub output: T::Type, 258 | } 259 | 260 | impl IntoPortable for FunctionMetadata { 261 | type Output = FunctionMetadata; 262 | 263 | fn into_portable(self, registry: &mut Registry) -> Self::Output { 264 | FunctionMetadata { 265 | name: self.name.into_portable(registry), 266 | inputs: registry.map_into_portable(self.inputs), 267 | output: registry.register_type(&self.output), 268 | } 269 | } 270 | } 271 | 272 | #[derive(Clone, PartialEq, Eq, Encode, Debug, Serialize)] 273 | pub struct FunctionParamMetadata { 274 | pub name: T::String, 275 | pub ty: T::Type, 276 | } 277 | 278 | impl IntoPortable for FunctionParamMetadata { 279 | type Output = FunctionParamMetadata; 280 | 281 | fn into_portable(self, registry: &mut Registry) -> Self::Output { 282 | FunctionParamMetadata { 283 | name: self.name.into_portable(registry), 284 | ty: registry.register_type(&self.ty), 285 | } 286 | } 287 | } 288 | } 289 | } 290 | -------------------------------------------------------------------------------- /pvq-program/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pvq-program" 3 | description = "PVQ program utils" 4 | authors.workspace = true 5 | edition.workspace = true 6 | license.workspace = true 7 | repository.workspace = true 8 | version.workspace = true 9 | 10 | [dependencies] 11 | pvq-program-procedural = { path = "procedural" } 12 | parity-scale-codec = { workspace = true } 13 | scale-info = { workspace = true } 14 | polkavm-derive = { workspace = true } 15 | 16 | [features] 17 | default = ["std"] 18 | std = [] 19 | -------------------------------------------------------------------------------- /pvq-program/procedural/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pvq-program-procedural" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | proc-macro = true 8 | 9 | [dependencies] 10 | quote = { workspace = true } 11 | syn = { workspace = true } 12 | proc-macro2 = { workspace = true } 13 | proc-macro-crate = { workspace = true } 14 | -------------------------------------------------------------------------------- /pvq-program/procedural/src/lib.rs: -------------------------------------------------------------------------------- 1 | /// Declare the calls used in PVQ program 2 | /// ```ignore 3 | /// #[program] 4 | /// mod query_fungibles { 5 | /// // The types to be used in the program, which matches the runtime implementation 6 | /// type AssetId = u32; 7 | /// type AccountId = [u8; 32]; 8 | /// type Balance = u64; 9 | /// 10 | /// #[program::extension_fn(extension_id = 123456u64, fn_index = 1u8)] 11 | /// fn balance(asset: AssetId, who: AccountId) -> Balance; 12 | /// 13 | /// #[program::entrypoint] 14 | /// fn sum_balance(accounts: Vec) -> Balance { 15 | /// let mut sum = 0; 16 | /// for account in accounts { 17 | /// sum += balance(0, account); 18 | /// } 19 | /// sum 20 | /// } 21 | /// } 22 | /// ``` 23 | /// 24 | mod program; 25 | use proc_macro::TokenStream; 26 | 27 | #[proc_macro_attribute] 28 | pub fn program(attr: TokenStream, item: TokenStream) -> TokenStream { 29 | program::program(attr, item) 30 | } 31 | -------------------------------------------------------------------------------- /pvq-program/procedural/src/program/expand/mod.rs: -------------------------------------------------------------------------------- 1 | mod preludes; 2 | use super::{Def, ExtensionFn}; 3 | use proc_macro2::TokenStream as TokenStream2; 4 | use quote::{format_ident, quote, ToTokens}; 5 | pub fn expand(mut def: Def) -> TokenStream2 { 6 | let preludes = preludes::generate_preludes(&def); 7 | 8 | let expanded_extension_fns = def 9 | .extension_fns 10 | .iter_mut() 11 | .map(|extension_fn| expand_extension_fn(extension_fn, &def.parity_scale_codec)) 12 | .collect::>(); 13 | 14 | let main_fn = expand_main(&def); 15 | 16 | let new_items = quote! { 17 | #preludes 18 | #(#expanded_extension_fns)* 19 | #main_fn 20 | }; 21 | 22 | def.item 23 | .content 24 | .as_mut() 25 | .expect("This is checked by parsing") 26 | .1 27 | .push(syn::Item::Verbatim(new_items)); 28 | def.item.into_token_stream() 29 | } 30 | 31 | fn expand_extension_fn(extension_fn: &mut ExtensionFn, parity_scale_codec: &syn::Path) -> TokenStream2 { 32 | let extension_id = extension_fn.extension_id; 33 | let fn_index = extension_fn.fn_index; 34 | let fn_name = &extension_fn.item_fn.sig.ident; 35 | let args = &extension_fn.item_fn.sig.inputs; 36 | let enum_name = format_ident!("{}Call", fn_name); 37 | let expanded_enum = quote! ( 38 | #[allow(non_camel_case_types)] 39 | #[derive(#parity_scale_codec::Encode, #parity_scale_codec::Decode)] 40 | enum #enum_name { 41 | #[codec(index = #fn_index)] 42 | #fn_name { 43 | #args 44 | } 45 | } 46 | ); 47 | let arg_names = args 48 | .iter() 49 | .map(|arg| { 50 | let syn::FnArg::Typed(pat_type) = arg else { 51 | unreachable!("Checked in parse stage") 52 | }; 53 | &pat_type.pat 54 | }) 55 | .collect::>(); 56 | 57 | let fn_name_str = fn_name.to_string(); 58 | extension_fn.item_fn.block = Box::new(syn::parse_quote!( 59 | { 60 | let encoded_call = #parity_scale_codec::Encode::encode(&#enum_name::#fn_name { 61 | #(#arg_names),* 62 | }); 63 | let res = unsafe { 64 | host_call(#extension_id, encoded_call.as_ptr() as u32, encoded_call.len() as u32) 65 | }; 66 | let res_ptr = res as u32 as *const u8; 67 | let res_len = (res >> 32) as usize; 68 | let mut res_bytes = unsafe { core::slice::from_raw_parts(res_ptr, res_len) }; 69 | #parity_scale_codec::Decode::decode(&mut res_bytes).expect(concat!("Failed to decode result of ", #fn_name_str)) 70 | } 71 | )); 72 | let modified_extension_fn = &extension_fn.item_fn; 73 | quote!( 74 | #expanded_enum 75 | #modified_extension_fn 76 | ) 77 | } 78 | 79 | fn expand_main(def: &Def) -> TokenStream2 { 80 | let parity_scale_codec = &def.parity_scale_codec; 81 | 82 | // Get `ident: Type`s 83 | let arg_pats = def.entrypoint.item_fn.sig.inputs.iter().collect::>(); 84 | // Get `ident`s 85 | let arg_identifiers = arg_pats 86 | .iter() 87 | .map(|arg| { 88 | if let syn::FnArg::Typed(pat_type) = arg { 89 | pat_type.pat.to_token_stream() 90 | } else { 91 | unreachable!("Checked in parse stage") 92 | } 93 | }) 94 | .collect::>(); 95 | let arg_identifiers_str = arg_identifiers.iter().map(|arg| arg.to_string()).collect::>(); 96 | 97 | let decode_args = quote! { 98 | #(let #arg_pats = #parity_scale_codec::Decode::decode(&mut arg_bytes).expect(concat!("Failed to decode ", #arg_identifiers_str));)* 99 | }; 100 | 101 | let entrypoint_ident = &def.entrypoint.item_fn.sig.ident; 102 | let call_entrypoint = quote! { 103 | let res = #entrypoint_ident(#(#arg_identifiers),*); 104 | }; 105 | 106 | quote! { 107 | #[polkavm_derive::polkavm_export] 108 | extern "C" fn pvq(arg_ptr: u32, size: u32) -> u64 { 109 | let mut arg_bytes = unsafe { core::slice::from_raw_parts(arg_ptr as *const u8, size as usize) }; 110 | 111 | #decode_args 112 | 113 | #call_entrypoint 114 | 115 | let encoded_res = #parity_scale_codec::Encode::encode(&res); 116 | (encoded_res.len() as u64) << 32 | (encoded_res.as_ptr() as u64) 117 | 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /pvq-program/procedural/src/program/expand/preludes.rs: -------------------------------------------------------------------------------- 1 | use super::Def; 2 | use proc_macro2::TokenStream as TokenStream2; 3 | use quote::quote; 4 | 5 | pub fn generate_preludes(def: &Def) -> TokenStream2 { 6 | let extern_crate = quote! { 7 | extern crate alloc; 8 | }; 9 | 10 | let global_allocator = quote! { 11 | #[global_allocator] 12 | static GLOBAL: polkavm_derive::LeakingAllocator = polkavm_derive::LeakingAllocator; 13 | }; 14 | 15 | let panic_fn = quote! { 16 | #[panic_handler] 17 | fn panic(_info: &core::panic::PanicInfo) -> ! { 18 | unsafe { 19 | core::arch::asm!("unimp", options(noreturn)); 20 | } 21 | } 22 | }; 23 | 24 | let polkavm_derive = &def.polkavm_derive; 25 | 26 | let host_call_fn = quote! { 27 | #[#polkavm_derive::polkavm_import] 28 | extern "C" { 29 | fn host_call(extension_id:u64, call_ptr:u32, call_len: u32) -> u64; 30 | } 31 | }; 32 | 33 | quote! { 34 | 35 | #extern_crate 36 | 37 | #global_allocator 38 | 39 | #panic_fn 40 | 41 | #host_call_fn 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /pvq-program/procedural/src/program/mod.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::TokenStream; 2 | use syn::{parse_macro_input, ItemMod}; 3 | mod expand; 4 | mod parse; 5 | pub(crate) use parse::{Def, ExtensionFn}; 6 | 7 | pub fn program(_attr: TokenStream, item: TokenStream) -> TokenStream { 8 | let item = parse_macro_input!(item as ItemMod); 9 | match parse::Def::try_from(item) { 10 | Ok(def) => expand::expand(def).into(), 11 | Err(e) => e.to_compile_error().into(), 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /pvq-program/procedural/src/program/parse/entrypoint.rs: -------------------------------------------------------------------------------- 1 | use syn::spanned::Spanned; 2 | #[derive(Debug)] 3 | pub struct EntrypointDef { 4 | pub item_fn: syn::ItemFn, 5 | } 6 | 7 | impl EntrypointDef { 8 | pub fn try_from(_span: proc_macro2::Span, item: &mut syn::Item) -> syn::Result { 9 | if let syn::Item::Fn(item_fn) = item { 10 | if item_fn 11 | .sig 12 | .inputs 13 | .iter() 14 | .any(|arg| matches!(arg, syn::FnArg::Receiver(_))) 15 | { 16 | return Err(syn::Error::new( 17 | item_fn.span(), 18 | "Invalid program::entrypoint, expected fn args are not receiver type", 19 | )); 20 | } 21 | Ok(Self { 22 | item_fn: item_fn.clone(), 23 | }) 24 | } else { 25 | Err(syn::Error::new( 26 | item.span(), 27 | "Invalid program::entrypoint, expected item fn", 28 | )) 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /pvq-program/procedural/src/program/parse/extension_fn.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::Span; 2 | use syn::spanned::Spanned; 3 | 4 | #[derive(Debug)] 5 | pub struct ExtensionFn { 6 | pub item_fn: syn::ItemFn, 7 | pub extension_id: u64, 8 | pub fn_index: u32, 9 | } 10 | 11 | impl ExtensionFn { 12 | pub fn try_from( 13 | span: Span, 14 | item: syn::Item, 15 | extension_id: Option, 16 | fn_index: Option, 17 | ) -> syn::Result { 18 | let extension_id = extension_id.ok_or_else(|| { 19 | syn::Error::new( 20 | span, 21 | "Missing extension_id for program::extension_fn, expected #[program::extension_fn(extension_id = SOME_U64)]", 22 | ) 23 | })?; 24 | let item_fn = if let syn::Item::Fn(item_fn) = item { 25 | item_fn 26 | } else { 27 | return Err(syn::Error::new( 28 | item.span(), 29 | "Invalid program::extension_fn, expected item fn", 30 | )); 31 | }; 32 | // Check that the inputs of the function are all not self 33 | if item_fn 34 | .sig 35 | .inputs 36 | .iter() 37 | .any(|arg| matches!(arg, syn::FnArg::Receiver(_))) 38 | { 39 | return Err(syn::Error::new( 40 | item_fn.span(), 41 | "Invalid program::extension_fn, expected function inputs to not be receiver", 42 | )); 43 | } 44 | 45 | let fn_index = fn_index.ok_or_else(|| { 46 | syn::Error::new( 47 | span, 48 | "Missing fn_index for program::extension_fn, expected #[program::extension_fn(fn_index = SOME_U32)]", 49 | ) 50 | })?; 51 | Ok(Self { 52 | item_fn, 53 | extension_id, 54 | fn_index, 55 | }) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /pvq-program/procedural/src/program/parse/helper.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::Span; 2 | use proc_macro_crate::{crate_name, FoundCrate}; 3 | pub trait MutItemAttrs { 4 | fn mut_item_attrs(&mut self) -> Option<&mut Vec>; 5 | } 6 | /// Take the first item attribute (e.g. attribute like `#[pvq..]`) and decode it to `Attr` 7 | pub(crate) fn take_first_program_attr(item: &mut impl MutItemAttrs) -> syn::Result> { 8 | let Some(attrs) = item.mut_item_attrs() else { 9 | return Ok(None); 10 | }; 11 | 12 | let Some(index) = attrs.iter().position(|attr| { 13 | attr.path() 14 | .segments 15 | .first() 16 | .is_some_and(|segment| segment.ident == "program") 17 | }) else { 18 | return Ok(None); 19 | }; 20 | 21 | let pvq_attr = attrs.remove(index); 22 | Ok(Some(pvq_attr)) 23 | } 24 | impl MutItemAttrs for syn::Item { 25 | fn mut_item_attrs(&mut self) -> Option<&mut Vec> { 26 | match self { 27 | Self::Const(item) => Some(item.attrs.as_mut()), 28 | Self::Enum(item) => Some(item.attrs.as_mut()), 29 | Self::ExternCrate(item) => Some(item.attrs.as_mut()), 30 | Self::Fn(item) => Some(item.attrs.as_mut()), 31 | Self::ForeignMod(item) => Some(item.attrs.as_mut()), 32 | Self::Impl(item) => Some(item.attrs.as_mut()), 33 | Self::Macro(item) => Some(item.attrs.as_mut()), 34 | Self::Mod(item) => Some(item.attrs.as_mut()), 35 | Self::Static(item) => Some(item.attrs.as_mut()), 36 | Self::Struct(item) => Some(item.attrs.as_mut()), 37 | Self::Trait(item) => Some(item.attrs.as_mut()), 38 | Self::TraitAlias(item) => Some(item.attrs.as_mut()), 39 | Self::Type(item) => Some(item.attrs.as_mut()), 40 | Self::Union(item) => Some(item.attrs.as_mut()), 41 | Self::Use(item) => Some(item.attrs.as_mut()), 42 | _ => None, 43 | } 44 | } 45 | } 46 | 47 | impl MutItemAttrs for syn::TraitItem { 48 | fn mut_item_attrs(&mut self) -> Option<&mut Vec> { 49 | match self { 50 | Self::Const(item) => Some(item.attrs.as_mut()), 51 | Self::Fn(item) => Some(item.attrs.as_mut()), 52 | Self::Type(item) => Some(item.attrs.as_mut()), 53 | Self::Macro(item) => Some(item.attrs.as_mut()), 54 | _ => None, 55 | } 56 | } 57 | } 58 | 59 | impl MutItemAttrs for Vec { 60 | fn mut_item_attrs(&mut self) -> Option<&mut Vec> { 61 | Some(self) 62 | } 63 | } 64 | 65 | impl MutItemAttrs for syn::ItemMod { 66 | fn mut_item_attrs(&mut self) -> Option<&mut Vec> { 67 | Some(&mut self.attrs) 68 | } 69 | } 70 | 71 | impl MutItemAttrs for syn::ImplItemFn { 72 | fn mut_item_attrs(&mut self) -> Option<&mut Vec> { 73 | Some(&mut self.attrs) 74 | } 75 | } 76 | 77 | impl MutItemAttrs for syn::ItemType { 78 | fn mut_item_attrs(&mut self) -> Option<&mut Vec> { 79 | Some(&mut self.attrs) 80 | } 81 | } 82 | 83 | pub fn generate_crate_access(def_crate: &str) -> syn::Result { 84 | let ident = match crate_name(def_crate) { 85 | Ok(FoundCrate::Itself) => { 86 | let name = def_crate.replace('-', "_"); 87 | Ok(syn::Ident::new(&name, Span::call_site())) 88 | } 89 | Ok(FoundCrate::Name(name)) => Ok(syn::Ident::new(&name, Span::call_site())), 90 | Err(e) => Err(syn::Error::new(Span::call_site(), e)), 91 | }?; 92 | Ok(syn::Path::from(ident)) 93 | } 94 | -------------------------------------------------------------------------------- /pvq-program/procedural/src/program/parse/mod.rs: -------------------------------------------------------------------------------- 1 | use syn::spanned::Spanned; 2 | mod extension_fn; 3 | pub use extension_fn::ExtensionFn; 4 | mod entrypoint; 5 | pub use entrypoint::EntrypointDef; 6 | mod helper; 7 | // program definition 8 | pub struct Def { 9 | pub item: syn::ItemMod, 10 | pub extension_fns: Vec, 11 | pub entrypoint: EntrypointDef, 12 | pub parity_scale_codec: syn::Path, 13 | pub polkavm_derive: syn::Path, 14 | } 15 | 16 | impl Def { 17 | pub fn try_from(mut item: syn::ItemMod) -> syn::Result { 18 | let parity_scale_codec = helper::generate_crate_access("parity-scale-codec")?; 19 | let polkavm_derive = helper::generate_crate_access("polkavm-derive")?; 20 | let mod_span = item.span(); 21 | let items = &mut item 22 | .content 23 | .as_mut() 24 | .ok_or_else(|| { 25 | let msg = "Invalid #[program] definition, expected mod to be inline."; 26 | syn::Error::new(mod_span, msg) 27 | })? 28 | .1; 29 | 30 | let mut extension_fns = Vec::new(); 31 | let mut entrypoint = None; 32 | 33 | for i in (0..items.len()).rev() { 34 | let item = &mut items[i]; 35 | if let Some(attr) = helper::take_first_program_attr(item)? { 36 | if let Some(last_segment) = attr.path().segments.last() { 37 | if last_segment.ident == "extension_fn" { 38 | let mut extension_id = None; 39 | let mut fn_index = None; 40 | attr.parse_nested_meta(|meta| { 41 | if meta.path.is_ident("extension_id") { 42 | let value = meta.value()?; 43 | extension_id = Some(value.parse::()?.base10_parse::()?); 44 | } else if meta.path.is_ident("fn_index") { 45 | let value = meta.value()?; 46 | fn_index = Some(value.parse::()?.base10_parse::()?); 47 | } else { 48 | return Err(syn::Error::new( 49 | meta.path.span(), 50 | "Invalid attribute meta, expected `extension_id` or `fn_index`", 51 | )); 52 | } 53 | Ok(()) 54 | })?; 55 | 56 | let removed_item = items.remove(i); 57 | let extension_fn = ExtensionFn::try_from(attr.span(), removed_item, extension_id, fn_index)?; 58 | extension_fns.push(extension_fn); 59 | continue; 60 | } else if last_segment.ident == "entrypoint" { 61 | if entrypoint.is_some() { 62 | return Err(syn::Error::new(attr.span(), "Only one entrypoint function is allowed")); 63 | } 64 | entrypoint = Some(EntrypointDef::try_from(attr.span(), item)?); 65 | continue; 66 | } else { 67 | return Err(syn::Error::new( 68 | item.span(), 69 | "Invalid attribute, expected `#[program::extension_fn]` or `#[program::entrypoint]`", 70 | )); 71 | } 72 | } 73 | } 74 | } 75 | 76 | let entrypoint = 77 | entrypoint.ok_or_else(|| syn::Error::new(mod_span, "At least one entrypoint function is required"))?; 78 | let def = Def { 79 | item, 80 | extension_fns, 81 | entrypoint, 82 | parity_scale_codec, 83 | polkavm_derive, 84 | }; 85 | 86 | Ok(def) 87 | } 88 | } 89 | 90 | /// List of additional token to be used for parsing. 91 | mod keyword { 92 | syn::custom_keyword!(program); 93 | syn::custom_keyword!(extension_id); 94 | syn::custom_keyword!(fn_index); 95 | syn::custom_keyword!(entrypoint); 96 | } 97 | -------------------------------------------------------------------------------- /pvq-program/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(not(feature = "std"), no_std)] 2 | pub use pvq_program_procedural::program; 3 | -------------------------------------------------------------------------------- /pvq-runtime-api/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pvq-runtime-api" 3 | description = "Runtime API for PVQ" 4 | authors.workspace = true 5 | edition.workspace = true 6 | license.workspace = true 7 | repository.workspace = true 8 | version.workspace = true 9 | 10 | [dependencies] 11 | pvq-primitives = { workspace = true } 12 | 13 | sp-api = { workspace = true } 14 | 15 | [features] 16 | default = ["std"] 17 | std = ["pvq-primitives/std", "sp-api/std"] 18 | -------------------------------------------------------------------------------- /pvq-runtime-api/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(not(feature = "std"), no_std)] 2 | 3 | extern crate alloc; 4 | 5 | use alloc::vec::Vec; 6 | use pvq_primitives::PvqResult; 7 | use sp_api::decl_runtime_apis; 8 | 9 | // The runtime API for the PVQ module. 10 | // - `program`: PVQ binary. 11 | // - `args`: Query arguments that is SCALE-encoded. 12 | // - `gas_limit`: Optional gas limit for query execution. When set to `None`, execution is constrained by the default time boundary. 13 | decl_runtime_apis! { 14 | pub trait PvqApi { 15 | fn execute_query(program: Vec, args: Vec, gas_limit: Option) -> PvqResult; 16 | fn metadata() -> Vec; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /pvq-test-runner/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pvq-test-runner" 3 | description = "PVQ program runner. Only for testing purpose." 4 | authors.workspace = true 5 | edition.workspace = true 6 | license.workspace = true 7 | repository.workspace = true 8 | version.workspace = true 9 | 10 | [dependencies] 11 | clap = { workspace = true } 12 | tracing = { workspace = true, features = ["std"] } 13 | tracing-subscriber = { workspace = true } 14 | parity-scale-codec = { workspace = true, features = ["std"] } 15 | scale-info = { workspace = true, features = ["std", "serde"] } 16 | sp-core = { workspace = true, features = ["std"] } 17 | serde = { workspace = true } 18 | serde_json = { workspace = true } 19 | 20 | pvq-executor = { workspace = true, features = ["std"] } 21 | pvq-extension = { workspace = true, features = ["std"] } 22 | pvq-extension-core = { workspace = true, features = ["std"] } 23 | pvq-extension-fungibles = { workspace = true, features = ["std"] } 24 | pvq-extension-swap = { workspace = true, features = ["std"] } 25 | pvq-primitives = { workspace = true, features = ["std"] } 26 | 27 | polkavm = { workspace = true, features = ["std"] } 28 | -------------------------------------------------------------------------------- /pvq-test-runner/src/bin/pvq-test-runner.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use tracing_subscriber::prelude::*; 3 | 4 | use pvq_test_runner::TestRunner; 5 | 6 | #[derive(Parser, Debug)] 7 | #[command(version, about)] 8 | struct Cli { 9 | /// Path to the PolkaVM program to execute 10 | #[arg(short, long)] 11 | program: std::path::PathBuf, 12 | } 13 | 14 | fn main() { 15 | let registry = tracing_subscriber::registry(); 16 | 17 | let filter = tracing_subscriber::EnvFilter::builder() 18 | .with_default_directive(tracing::Level::INFO.into()) 19 | .from_env_lossy(); 20 | 21 | registry 22 | .with(tracing_subscriber::fmt::layer().with_filter(filter)) 23 | .try_init() 24 | .expect("Failed to initialize tracing"); 25 | 26 | let cli = Cli::parse(); 27 | 28 | let blob = std::fs::read(&cli.program).expect("Failed to read program"); 29 | let program_str = cli.program.to_string_lossy(); 30 | 31 | let input_data = TestRunner::prepare_input_data(&program_str); 32 | tracing::info!("Input data: {:?}", input_data); 33 | 34 | let mut runner = TestRunner::new(); 35 | let res = runner.execute_program(&blob, &input_data).unwrap(); 36 | 37 | let metadata = pvq_test_runner::extensions::metadata(); 38 | let metadata_json = serde_json::to_string(&metadata).expect("Failed to serialize metadata"); 39 | tracing::info!("Metadata: {}", metadata_json); 40 | 41 | tracing::info!("Result: {:?}", res); 42 | } 43 | -------------------------------------------------------------------------------- /pvq-test-runner/src/lib.rs: -------------------------------------------------------------------------------- 1 | use parity_scale_codec::Encode; 2 | use pvq_extension::{extensions_impl, ExtensionsExecutor, InvokeSource}; 3 | use sp_core::crypto::{AccountId32, Ss58Codec}; 4 | use sp_core::hexdisplay::HexDisplay; 5 | 6 | #[derive(Encode)] 7 | #[allow(non_camel_case_types)] 8 | #[allow(dead_code)] 9 | pub enum ExtensionFungiblesFunctions { 10 | #[codec(index = 0)] 11 | total_supply { asset: u32 }, 12 | #[codec(index = 1)] 13 | balance { asset: u32, who: [u8; 32] }, 14 | } 15 | 16 | #[extensions_impl] 17 | pub mod extensions { 18 | use parity_scale_codec::Decode; 19 | #[extensions_impl::impl_struct] 20 | pub struct ExtensionsImpl; 21 | 22 | #[extensions_impl::extension] 23 | impl pvq_extension_core::extension::ExtensionCore for ExtensionsImpl { 24 | type ExtensionId = u64; 25 | fn has_extension(id: Self::ExtensionId) -> bool { 26 | matches!(id, 0 | 1) 27 | } 28 | } 29 | 30 | #[extensions_impl::extension] 31 | impl pvq_extension_fungibles::extension::ExtensionFungibles for ExtensionsImpl { 32 | type AssetId = u32; 33 | type AccountId = [u8; 32]; 34 | type Balance = u64; 35 | fn total_supply(_asset: Self::AssetId) -> Self::Balance { 36 | 100 37 | } 38 | fn balance(_asset: Self::AssetId, _who: Self::AccountId) -> Self::Balance { 39 | 100 40 | } 41 | } 42 | #[extensions_impl::extension] 43 | impl pvq_extension_swap::extension::ExtensionSwap for ExtensionsImpl { 44 | type AssetId = Vec; 45 | type Balance = u64; 46 | fn quote_price_tokens_for_exact_tokens( 47 | _asset1: Self::AssetId, 48 | _asset2: Self::AssetId, 49 | _amount: Self::Balance, 50 | _include_fee: bool, 51 | ) -> Option { 52 | None 53 | } 54 | 55 | fn quote_price_exact_tokens_for_tokens( 56 | _asset1: Self::AssetId, 57 | _asset2: Self::AssetId, 58 | _amount: Self::Balance, 59 | _include_fee: bool, 60 | ) -> Option { 61 | None 62 | } 63 | 64 | fn get_liquidity_pool(asset1: Self::AssetId, asset2: Self::AssetId) -> Option<(Self::Balance, Self::Balance)> { 65 | let _asset1 = u32::decode(&mut &asset1[..]).expect("Failed to decode asset1"); 66 | let _asset2 = u32::decode(&mut &asset2[..]).expect("Failed to decode asset2"); 67 | Some((100, 100)) 68 | } 69 | 70 | fn list_pools() -> Vec<(Self::AssetId, Self::AssetId, Self::Balance, Self::Balance)> { 71 | vec![] 72 | } 73 | } 74 | } 75 | 76 | pub struct TestRunner { 77 | executor: ExtensionsExecutor, 78 | } 79 | 80 | impl TestRunner { 81 | pub fn new() -> Self { 82 | Self { 83 | executor: ExtensionsExecutor::new(InvokeSource::RuntimeAPI), 84 | } 85 | } 86 | 87 | pub fn prepare_input_data(program_path: &str) -> Vec { 88 | let mut input_data = Vec::new(); 89 | 90 | if program_path.contains("sum-balance") { 91 | input_data.extend_from_slice(&21u32.encode()); 92 | 93 | let alice_account: [u8; 32] = 94 | AccountId32::from_ss58check("5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY") 95 | .expect("Failed to decode Alice's address") 96 | .into(); 97 | input_data.extend_from_slice(&vec![alice_account].encode()); 98 | } else if program_path.contains("total-supply") { 99 | input_data.extend_from_slice(&21u32.encode()); 100 | } else if program_path.contains("transparent-call") { 101 | input_data.extend_from_slice(&4071833530116166512u64.encode()); 102 | let alice_account = AccountId32::from_ss58check("5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY") 103 | .expect("Failed to decode Alice's address"); 104 | input_data.extend_from_slice( 105 | &ExtensionFungiblesFunctions::balance { 106 | asset: 21u32, 107 | who: alice_account.into(), 108 | } 109 | .encode(), 110 | ); 111 | } else if program_path.contains("liquidity-pool") { 112 | let asset1 = u32::encode(&21); 113 | let asset2 = u32::encode(&22); 114 | input_data.extend_from_slice(&asset1.encode()); 115 | input_data.extend_from_slice(&asset2.encode()); 116 | } 117 | tracing::info!("Input data (hex): {}", HexDisplay::from(&input_data)); 118 | input_data 119 | } 120 | 121 | pub fn execute_program(&mut self, program_blob: &[u8], input_data: &[u8]) -> pvq_primitives::PvqResult { 122 | let (result, _) = self.executor.execute(program_blob, input_data, None); 123 | result 124 | } 125 | } 126 | 127 | impl Default for TestRunner { 128 | fn default() -> Self { 129 | Self::new() 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "nightly-2024-11-19" 3 | components = ["rust-src", "rustfmt", "clippy"] 4 | targets = ["wasm32-unknown-unknown"] 5 | --------------------------------------------------------------------------------