fix: Convert vendor/pezkuwi-subxt from submodule to regular directory
This commit is contained in:
Vendored
-1
Submodule vendor/pezkuwi-subxt deleted from 545b8ae818
Vendored
+16
@@ -0,0 +1,16 @@
|
||||
# EditorConfig helps developers define and maintain consistent
|
||||
# coding styles between different editors and IDEs
|
||||
# editorconfig.org
|
||||
|
||||
root = true
|
||||
|
||||
[*]
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
[*.yml]
|
||||
indent_size = 2
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "cargo"
|
||||
directories:
|
||||
- "**/*"
|
||||
schedule:
|
||||
interval: weekly
|
||||
- package-ecosystem: github-actions
|
||||
directories:
|
||||
- "**/*"
|
||||
schedule:
|
||||
interval: weekly
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
title: Subxt integration tests failed against latest Substrate build.
|
||||
---
|
||||
|
||||
The nightly CI run which downloads the latest version of Substrate ran into test failures, which likely means that there are breaking changes that need fixing in Subxt.
|
||||
|
||||
Go to https://github.com/paritytech/subxt/actions/workflows/nightly.yml to see details about the failure.
|
||||
@@ -0,0 +1,3 @@
|
||||
# use-nodes
|
||||
|
||||
This action downloads the substrate and polkadot binaries produced from the `build-nodes` workflow and puts them into the `$PATH`.
|
||||
@@ -0,0 +1,43 @@
|
||||
name: Use substrate and polkadot binaries
|
||||
description: Downloads and configures the substrate and polkadot binaries built with `build-nodes`
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Install dependencies
|
||||
shell: bash
|
||||
run: sudo apt-get update && sudo apt-get install -y curl gcc make clang cmake
|
||||
|
||||
- name: Download substrate-node binary
|
||||
id: download-substrate-binary
|
||||
uses: dawidd6/action-download-artifact@4c1e823582f43b179e2cbb49c3eade4e41f992e2 # v10
|
||||
with:
|
||||
workflow: build-nodes.yml
|
||||
name: nightly-substrate-binary
|
||||
|
||||
- name: Download polkadot binary
|
||||
id: download-polkadot-binary
|
||||
uses: dawidd6/action-download-artifact@4c1e823582f43b179e2cbb49c3eade4e41f992e2 # v10
|
||||
with:
|
||||
workflow: build-nodes.yml
|
||||
name: nightly-polkadot-binary
|
||||
|
||||
- name: decompress polkadot binary
|
||||
shell: bash
|
||||
run: |
|
||||
tar -xzvf ./polkadot.tar.gz
|
||||
cp ./target/release/polkadot ./polkadot
|
||||
|
||||
- name: Prepare binaries
|
||||
shell: bash
|
||||
run: |
|
||||
chmod u+x ./substrate-node
|
||||
chmod u+x ./polkadot
|
||||
chmod u+x ./polkadot-execute-worker
|
||||
chmod u+x ./polkadot-prepare-worker
|
||||
./substrate-node --version
|
||||
./polkadot --version
|
||||
sudo mv ./substrate-node /usr/local/bin
|
||||
sudo mv ./polkadot /usr/local/bin
|
||||
sudo mv ./polkadot-execute-worker /usr/local/bin
|
||||
sudo mv ./polkadot-prepare-worker /usr/local/bin
|
||||
rm ./polkadot.tar.gz
|
||||
@@ -0,0 +1,73 @@
|
||||
name: Build Substrate and Polkadot Binaries
|
||||
|
||||
on:
|
||||
# Allow it to be manually ran to rebuild binary when needed:
|
||||
workflow_dispatch: {}
|
||||
# Run at 2am every day for nightly builds.
|
||||
schedule:
|
||||
- cron: "0 2 * * *"
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
name: Build Substrate and Polkadot Binaries
|
||||
runs-on: parity-large
|
||||
steps:
|
||||
- name: checkout polkadot-sdk
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: paritytech/polkadot-sdk
|
||||
|
||||
- name: Install dependencies
|
||||
run: sudo apt-get update && sudo apt-get install -y protobuf-compiler curl gcc make clang cmake llvm-dev libclang-dev
|
||||
|
||||
- name: Install Rust v1.88 toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: 1.88
|
||||
components: rust-src
|
||||
target: wasm32-unknown-unknown
|
||||
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
|
||||
- name: build substrate binary
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: build
|
||||
args: --release --manifest-path substrate/bin/node/cli/Cargo.toml
|
||||
|
||||
- name: build polkadot binary
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: build
|
||||
args: --release --manifest-path polkadot/Cargo.toml
|
||||
|
||||
- name: Strip binaries
|
||||
run: |
|
||||
cargo install cargo-strip
|
||||
cargo strip
|
||||
|
||||
- name: upload substrate binary
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: nightly-substrate-binary
|
||||
path: target/release/substrate-node
|
||||
retention-days: 2
|
||||
if-no-files-found: error
|
||||
|
||||
# Note: Uncompressed polkadot binary is ~124MB -> too large for git (max 100MB) without git lfs. Compressed it is only ~45MB
|
||||
- name: compress polkadot binary
|
||||
run: |
|
||||
tar -zcvf target/release/polkadot.tar.gz target/release/polkadot
|
||||
|
||||
- name: upload polkadot binary
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: nightly-polkadot-binary
|
||||
path: |
|
||||
target/release/polkadot.tar.gz
|
||||
target/release/polkadot-execute-worker
|
||||
target/release/polkadot-prepare-worker
|
||||
retention-days: 2
|
||||
if-no-files-found: error
|
||||
@@ -0,0 +1,51 @@
|
||||
name: Daily compatibility check against latest substrate
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Run at 8am every day, well after the new binary is built
|
||||
- cron: "0 8 * * *"
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
name: Cargo test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Use substrate and polkadot node binaries
|
||||
uses: ./.github/workflows/actions/use-nodes
|
||||
|
||||
- name: Install Rust stable toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
|
||||
- name: Cargo test
|
||||
uses: actions-rs/cargo@v1.0.3
|
||||
with:
|
||||
command: test
|
||||
args: --all-targets --workspace
|
||||
|
||||
# If any previous step fails, create a new Github issue to notify us about it.
|
||||
- if: ${{ failure() }}
|
||||
uses: JasonEtco/create-an-issue@1b14a70e4d8dc185e5cc76d3bec9eab20257b2c5 # v2.9.2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
# Use this issue template:
|
||||
filename: .github/issue_templates/nightly_run_failed.md
|
||||
# Update existing issue if found; hopefully will make it clearer
|
||||
# that it is still an issue:
|
||||
update_existing: true
|
||||
# Look for new *open* issues in this search (we want to
|
||||
# create a new one if we only find closed versions):
|
||||
search_existing: open
|
||||
+529
@@ -0,0 +1,529 @@
|
||||
name: Rust
|
||||
|
||||
on:
|
||||
push:
|
||||
# Run jobs when commits are pushed to
|
||||
# master or release-like branches:
|
||||
branches:
|
||||
- master
|
||||
# If we want to backport changes to an old release, push a branch
|
||||
# eg v0.40.x and CI will run on it. PRs merging to such branches
|
||||
# will also trigger CI.
|
||||
- v0.[0-9]+.x
|
||||
pull_request:
|
||||
# Run jobs for any external PR that wants
|
||||
# to merge to master, too:
|
||||
branches:
|
||||
- master
|
||||
- v0.[0-9]+.x
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.ref }}-${{ github.workflow }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
# Increase wasm test timeout from 20 seconds (default) to 1 minute.
|
||||
WASM_BINDGEN_TEST_TIMEOUT: 60
|
||||
|
||||
jobs:
|
||||
fmt:
|
||||
name: Cargo fmt
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Rust nightly toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
components: rustfmt
|
||||
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
|
||||
- name: Cargo fmt
|
||||
uses: actions-rs/cargo@v1.0.3
|
||||
with:
|
||||
command: fmt
|
||||
args: --all -- --check
|
||||
|
||||
- if: "failure()"
|
||||
uses: "andymckay/cancel-action@a955d435292c0d409d104b57d8e78435a93a6ef1" # v0.5
|
||||
|
||||
machete:
|
||||
name: "Check unused dependencies"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Use substrate and polkadot node binaries
|
||||
uses: ./.github/workflows/actions/use-nodes
|
||||
|
||||
- name: Install Rust stable toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
|
||||
- name: Install cargo-machete
|
||||
run: cargo install cargo-machete
|
||||
|
||||
- name: Check unused dependencies
|
||||
uses: actions-rs/cargo@v1.0.3
|
||||
with:
|
||||
command: machete
|
||||
|
||||
- if: "failure()"
|
||||
uses: "andymckay/cancel-action@a955d435292c0d409d104b57d8e78435a93a6ef1" # v0.5
|
||||
|
||||
clippy:
|
||||
name: Cargo clippy
|
||||
runs-on: parity-large
|
||||
needs: [fmt, machete]
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Use substrate and polkadot node binaries
|
||||
uses: ./.github/workflows/actions/use-nodes
|
||||
|
||||
- name: Install Rust stable toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
components: clippy
|
||||
override: true
|
||||
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
|
||||
- name: Run clippy
|
||||
run: |
|
||||
cargo clippy --all-targets --features unstable-light-client -- -D warnings
|
||||
cargo clippy -p subxt-lightclient --no-default-features --features web -- -D warnings
|
||||
cargo clippy -p subxt --no-default-features --features web -- -D warnings
|
||||
cargo clippy -p subxt --no-default-features --features web,unstable-light-client -- -D warnings
|
||||
|
||||
- if: "failure()"
|
||||
uses: "andymckay/cancel-action@a955d435292c0d409d104b57d8e78435a93a6ef1" # v0.5
|
||||
|
||||
wasm_clippy:
|
||||
name: Cargo clippy (WASM)
|
||||
runs-on: ubuntu-latest
|
||||
needs: [fmt, machete]
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Use substrate and polkadot node binaries
|
||||
uses: ./.github/workflows/actions/use-nodes
|
||||
|
||||
- name: Install Rust stable toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
target: wasm32-unknown-unknown
|
||||
override: true
|
||||
components: clippy
|
||||
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
|
||||
- name: Run clippy
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: clippy
|
||||
args: -p subxt --no-default-features --features web,unstable-light-client,jsonrpsee --target wasm32-unknown-unknown -- -D warnings
|
||||
|
||||
- if: "failure()"
|
||||
uses: "andymckay/cancel-action@a955d435292c0d409d104b57d8e78435a93a6ef1" # v0.5
|
||||
|
||||
check:
|
||||
name: Cargo check
|
||||
runs-on: parity-large
|
||||
needs: [fmt, machete]
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Use substrate and polkadot node binaries
|
||||
uses: ./.github/workflows/actions/use-nodes
|
||||
|
||||
- name: Install Rust stable toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
|
||||
- name: Install cargo-hack
|
||||
uses: baptiste0928/cargo-install@v3
|
||||
with:
|
||||
crate: cargo-hack
|
||||
version: 0.5
|
||||
|
||||
# A basic check over all targets together. This may lead to features being combined etc,
|
||||
# and doesn't test combinations of different features.
|
||||
- name: Cargo check all targets.
|
||||
run: cargo check --all-targets
|
||||
|
||||
# Next, check each subxt feature in isolation.
|
||||
# - `native` feature must always be enabled
|
||||
# - `web` feature is always ignored.
|
||||
- name: Cargo hack; check each subxt feature
|
||||
run: cargo hack -p subxt --each-feature check --exclude-features web --features native
|
||||
|
||||
# Same with subxt-historic
|
||||
- name: Cargo hack; check each subxt feature
|
||||
run: cargo hack -p subxt-historic --each-feature check --exclude-features web --features native
|
||||
|
||||
# And with subxt-rpcs
|
||||
- name: Cargo hack; check each subxt-rpcs feature
|
||||
run: cargo hack -p subxt-rpcs --each-feature check --exclude-features web --features native
|
||||
|
||||
# And with subxt-signer (seems to work with a more basic check here; disable web if it becomes an issue).
|
||||
- name: Cargo hack; check each subxt-signer feature
|
||||
run: cargo hack -p subxt-signer --each-feature check
|
||||
|
||||
# And for subxt-lightclient.
|
||||
- name: Cargo check subxt-lightclient
|
||||
run: cargo hack -p subxt-lightclient --each-feature check --exclude-features web --features native
|
||||
|
||||
# Next, check all other crates.
|
||||
- name: Cargo hack; check each feature/crate on its own
|
||||
run: cargo hack --exclude subxt --exclude subxt-historic --exclude subxt-signer --exclude subxt-lightclient --exclude subxt-rpcs --exclude-all-features --each-feature check --workspace
|
||||
|
||||
# Check the full examples, which aren't a part of the workspace so are otherwise ignored.
|
||||
- name: Cargo check parachain-example
|
||||
run: cargo check --manifest-path examples/parachain-example/Cargo.toml
|
||||
- name: Cargo check ffi-example
|
||||
run: cargo check --manifest-path examples/ffi-example/Cargo.toml
|
||||
|
||||
- if: "failure()"
|
||||
uses: "andymckay/cancel-action@a955d435292c0d409d104b57d8e78435a93a6ef1" # v0.5
|
||||
|
||||
ffi_example:
|
||||
name: Run FFI Example
|
||||
runs-on: ubuntu-latest
|
||||
needs: [check]
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Use substrate and polkadot node binaries
|
||||
uses: ./.github/workflows/actions/use-nodes
|
||||
|
||||
- name: Install Rust stable toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
target: wasm32-unknown-unknown
|
||||
override: true
|
||||
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
|
||||
- name: Install
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
# Node version 20 and higher seem to cause an issue with the JS example so stick to 19 for now.
|
||||
node-version: 19.x
|
||||
|
||||
- name: Cargo check/run ffi-example
|
||||
run: |
|
||||
# Start node on port 8000
|
||||
substrate-node --dev --rpc-port 8000 > /dev/null 2>&1 &
|
||||
|
||||
# Build the Rust code (hopefully gives long enough for substrate server to start, too):
|
||||
cd examples/ffi-example
|
||||
cargo build
|
||||
|
||||
# Run the python version of the FFI code:
|
||||
echo "Running Python FFI example..."
|
||||
python3 src/main.py
|
||||
echo "Python FFI example completed with exit code $?"
|
||||
|
||||
# Run the node version of the FFI code
|
||||
echo "Installing Node.js dependencies..."
|
||||
npm i
|
||||
echo "Running Node FFI example..."
|
||||
node src/main.js
|
||||
echo "Node FFI example completed with exit code $?"
|
||||
|
||||
pkill substrate-node
|
||||
|
||||
wasm_check:
|
||||
name: Cargo check (WASM)
|
||||
runs-on: ubuntu-latest
|
||||
needs: [fmt, machete]
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Rust stable toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
target: wasm32-unknown-unknown
|
||||
override: true
|
||||
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
|
||||
- name: Cargo check web features which require wasm32 target.
|
||||
run: |
|
||||
cargo check -p subxt-rpcs --target wasm32-unknown-unknown --no-default-features --features web
|
||||
cargo check -p subxt-rpcs --target wasm32-unknown-unknown --no-default-features --features web,reconnecting-rpc-client
|
||||
|
||||
# Check WASM examples, which aren't a part of the workspace and so are otherwise missed:
|
||||
- name: Cargo check WASM examples
|
||||
run: |
|
||||
cargo check --manifest-path examples/wasm-example/Cargo.toml --target wasm32-unknown-unknown
|
||||
|
||||
- if: "failure()"
|
||||
uses: "andymckay/cancel-action@a955d435292c0d409d104b57d8e78435a93a6ef1" # v0.5
|
||||
|
||||
docs:
|
||||
name: Check documentation and run doc tests
|
||||
runs-on: parity-large
|
||||
needs: [fmt, machete]
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Use substrate and polkadot node binaries
|
||||
uses: ./.github/workflows/actions/use-nodes
|
||||
|
||||
- name: Install Rust stable toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
|
||||
- name: Check internal documentation links
|
||||
run: RUSTDOCFLAGS="--deny rustdoc::broken_intra_doc_links" cargo doc --workspace --no-deps --document-private-items
|
||||
|
||||
- name: Run cargo test on documentation
|
||||
uses: actions-rs/cargo@v1.0.3
|
||||
with:
|
||||
command: test
|
||||
args: --doc --features reconnecting-rpc-client
|
||||
|
||||
- if: "failure()"
|
||||
uses: "andymckay/cancel-action@a955d435292c0d409d104b57d8e78435a93a6ef1" # v0.5
|
||||
|
||||
tests:
|
||||
name: "Test (Native)"
|
||||
runs-on: parity-large
|
||||
needs: [clippy, wasm_clippy, check, wasm_check, docs]
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Use substrate and polkadot node binaries
|
||||
uses: ./.github/workflows/actions/use-nodes
|
||||
|
||||
- name: Install Rust stable toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
|
||||
- name: Install cargo-nextest
|
||||
run: cargo install cargo-nextest
|
||||
|
||||
- name: Run subxt-signer no-std tests
|
||||
uses: actions-rs/cargo@v1.0.3
|
||||
with:
|
||||
command: test
|
||||
working-directory: signer/tests/no-std
|
||||
|
||||
- name: Run tests
|
||||
uses: actions-rs/cargo@v1.0.3
|
||||
with:
|
||||
command: nextest
|
||||
args: run --workspace --features reconnecting-rpc-client
|
||||
|
||||
- if: "failure()"
|
||||
uses: "andymckay/cancel-action@a955d435292c0d409d104b57d8e78435a93a6ef1" # v0.5
|
||||
|
||||
unstable_backend_tests:
|
||||
name: "Test chainhead backend"
|
||||
runs-on: parity-large
|
||||
needs: [clippy, wasm_clippy, check, wasm_check, docs]
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Use substrate and polkadot node binaries
|
||||
uses: ./.github/workflows/actions/use-nodes
|
||||
|
||||
- name: Install Rust stable toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
|
||||
- name: Install cargo-nextest
|
||||
run: cargo install cargo-nextest
|
||||
|
||||
- name: Run tests
|
||||
uses: actions-rs/cargo@v1.0.3
|
||||
with:
|
||||
command: nextest
|
||||
args: run --workspace --features chainhead-backend
|
||||
|
||||
- if: "failure()"
|
||||
uses: "andymckay/cancel-action@a955d435292c0d409d104b57d8e78435a93a6ef1" # v0.5
|
||||
|
||||
light_client_tests:
|
||||
name: "Test (Light Client)"
|
||||
runs-on: ubuntu-latest
|
||||
needs: [clippy, wasm_clippy, check, wasm_check, docs]
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Use substrate and polkadot node binaries
|
||||
uses: ./.github/workflows/actions/use-nodes
|
||||
|
||||
- name: Install Rust stable toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
|
||||
- name: Run tests
|
||||
uses: actions-rs/cargo@v1.0.3
|
||||
with:
|
||||
command: test
|
||||
args: --release --package integration-tests --features unstable-light-client
|
||||
|
||||
- if: "failure()"
|
||||
uses: "andymckay/cancel-action@a955d435292c0d409d104b57d8e78435a93a6ef1" # v0.5
|
||||
|
||||
wasm_tests:
|
||||
name: Test (WASM)
|
||||
runs-on: ubuntu-latest
|
||||
needs: [clippy, wasm_clippy, check, wasm_check, docs]
|
||||
timeout-minutes: 30
|
||||
env:
|
||||
# Set timeout for wasm tests to be much bigger than the default 20 secs.
|
||||
WASM_BINDGEN_TEST_TIMEOUT: 300
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Install wasm-pack
|
||||
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
|
||||
|
||||
- name: Install firefox
|
||||
uses: browser-actions/setup-firefox@latest
|
||||
|
||||
- name: Install chrome
|
||||
uses: browser-actions/setup-chrome@latest
|
||||
|
||||
- name: Use substrate and polkadot node binaries
|
||||
uses: ./.github/workflows/actions/use-nodes
|
||||
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
|
||||
- name: Run subxt WASM tests
|
||||
run: |
|
||||
# `listen-addr` is used to configure p2p to accept websocket connections instead of TCP.
|
||||
# `node-key` provides a deterministic p2p address.
|
||||
substrate-node --dev --node-key 0000000000000000000000000000000000000000000000000000000000000001 --listen-addr /ip4/0.0.0.0/tcp/30333/ws > /dev/null 2>&1 &
|
||||
wasm-pack test --headless --firefox
|
||||
wasm-pack test --headless --chrome
|
||||
pkill substrate-node
|
||||
working-directory: testing/wasm-rpc-tests
|
||||
|
||||
- name: Run subxt-lightclient WASM tests
|
||||
run: |
|
||||
# `listen-addr` is used to configure p2p to accept websocket connections instead of TCP.
|
||||
# `node-key` provides a deterministic p2p address.
|
||||
substrate-node --dev --node-key 0000000000000000000000000000000000000000000000000000000000000001 --listen-addr /ip4/0.0.0.0/tcp/30333/ws > /dev/null 2>&1 &
|
||||
wasm-pack test --headless --firefox
|
||||
wasm-pack test --headless --chrome
|
||||
pkill substrate-node
|
||||
working-directory: testing/wasm-lightclient-tests
|
||||
|
||||
- name: Run subxt-signer WASM tests
|
||||
run: |
|
||||
wasm-pack test --headless --firefox
|
||||
wasm-pack test --headless --chrome
|
||||
working-directory: signer/tests/wasm
|
||||
|
||||
- if: "failure()"
|
||||
uses: "andymckay/cancel-action@a955d435292c0d409d104b57d8e78435a93a6ef1" # v0.5
|
||||
|
||||
no-std-tests:
|
||||
name: "Test (no_std)"
|
||||
runs-on: ubuntu-latest
|
||||
needs: [machete, docs]
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v6
|
||||
|
||||
# Note: needs nighly toolchain because otherwise we cannot define custom lang-items.
|
||||
- name: Install Rust nightly toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: nightly
|
||||
override: true
|
||||
target: thumbv7em-none-eabi
|
||||
|
||||
- name: Install the gcc-arm-none-eabi linker
|
||||
run: sudo apt install gcc-arm-none-eabi
|
||||
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
|
||||
# Note: We currently do not have a way to run real tests in a `no_std` environment.
|
||||
# We can only make sure that they compile to ARM thumb ISA.
|
||||
# Running the binary and inspecting the output would require an actual machine with matching ISA or some sort of emulator.
|
||||
- name: Compile `no-std-tests` crate to `thumbv7em-none-eabi` target.
|
||||
run: cargo build --target thumbv7em-none-eabi
|
||||
working-directory: testing/no-std-tests
|
||||
|
||||
- if: "failure()"
|
||||
uses: "andymckay/cancel-action@a955d435292c0d409d104b57d8e78435a93a6ef1" # v0.5
|
||||
@@ -0,0 +1,62 @@
|
||||
name: Update Artifacts
|
||||
|
||||
on:
|
||||
workflow_dispatch: # Allows manual triggering
|
||||
schedule:
|
||||
- cron: "0 0 * * 1" # weekly on Monday at 00:00 UTC
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.ref }}-${{ github.workflow }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: Renew Artifacts
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v6
|
||||
# We run this (up-to-date) node locally to fetch metadata from it for the artifacts
|
||||
- name: Use substrate and polkadot node binaries
|
||||
uses: ./.github/workflows/actions/use-nodes
|
||||
|
||||
- name: Install Rust stable toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
|
||||
# This starts a substrate node and runs a few subxt cli child processes to fetch metadata from it and generate code.
|
||||
# In particular it generates:
|
||||
# - 4 metadata (*.scale) files in the `artifacts` directory
|
||||
# - a polkadot.rs file from the full metadata that is checked in integration tests
|
||||
# - a polkadot.json in the `artifacts/demo_chain_specs` directory
|
||||
- name: Fetch Artifacts
|
||||
run: cargo run --bin artifacts
|
||||
|
||||
- uses: actions/create-github-app-token@v2
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ secrets.SUBXT_PR_MAKER_APP_ID }}
|
||||
private-key: ${{ secrets.SUBXT_PR_MAKER_APP_KEY }}
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
base: master
|
||||
branch: update-artifacts
|
||||
commit-message: Update Artifacts (auto-generated)
|
||||
branch-suffix: timestamp
|
||||
title: Update Artifacts (auto-generated)
|
||||
body: |
|
||||
This PR updates the artifacts by fetching fresh metadata from a substrate node.
|
||||
It also recreates the polkadot.rs file used in the integration tests.
|
||||
It was created automatically by a Weekly GitHub Action Cronjob.
|
||||
@@ -0,0 +1,14 @@
|
||||
name: Dependabot
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/dependabot.yml'
|
||||
- '.github/workflows/validate-dependabot.yml'
|
||||
jobs:
|
||||
validate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: marocchino/validate-dependabot@v3
|
||||
id: validate
|
||||
@@ -0,0 +1,9 @@
|
||||
/target
|
||||
**/*.rs.bk
|
||||
**/.DS_Store
|
||||
cargo-timing*
|
||||
/examples/wasm-example/dist
|
||||
/examples/wasm-example/target
|
||||
/examples/parachain-example/target
|
||||
/examples/parachain-example/metadata/target
|
||||
.vscode
|
||||
Vendored
+2401
File diff suppressed because it is too large
Load Diff
Vendored
+23
@@ -0,0 +1,23 @@
|
||||
# Lists some code owners.
|
||||
#
|
||||
# A codeowner just oversees some part of the codebase. If an owned file is changed then the
|
||||
# corresponding codeowner receives a review request. An approval of the codeowner might be
|
||||
# required for merging a PR (depends on repository settings).
|
||||
#
|
||||
# For details about syntax, see:
|
||||
# https://help.github.com/en/articles/about-code-owners
|
||||
# But here are some important notes:
|
||||
#
|
||||
# - Glob syntax is git-like, e.g. `/core` means the core directory in the root, unlike `core`
|
||||
# which can be everywhere.
|
||||
# - Multiple owners are supported.
|
||||
# - Either handle (e.g, @github_user or @github_org/team) or email can be used. Keep in mind,
|
||||
# that handles might work better because they are more recognizable on GitHub,
|
||||
# you can use them for mentioning unlike an email.
|
||||
# - The latest matching rule, if multiple, takes precedence.
|
||||
|
||||
# main codeowner
|
||||
* @paritytech/subxt-team
|
||||
|
||||
# CI
|
||||
/.github/ @paritytech/ci @paritytech/subxt-team
|
||||
+7300
File diff suppressed because it is too large
Load Diff
Vendored
+182
@@ -0,0 +1,182 @@
|
||||
[workspace]
|
||||
# All pezkuwi-subxt crates
|
||||
members = [
|
||||
"cli",
|
||||
"codegen",
|
||||
"core",
|
||||
"lightclient",
|
||||
"macro",
|
||||
"metadata",
|
||||
"rpcs",
|
||||
"signer",
|
||||
"subxt",
|
||||
"utils/fetch-metadata",
|
||||
"utils/strip-metadata",
|
||||
]
|
||||
|
||||
# Exclude testing and examples for now
|
||||
exclude = [
|
||||
"historic",
|
||||
"testing",
|
||||
"scripts",
|
||||
"examples",
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
authors = ["Pezkuwi Chain <admin@pezkuwichain.io>"]
|
||||
edition = "2024"
|
||||
version = "0.44.0"
|
||||
rust-version = "1.85.0"
|
||||
license = "Apache-2.0 OR GPL-3.0"
|
||||
repository = "https://github.com/pezkuwichain/pezkuwi-subxt"
|
||||
documentation = "https://docs.rs/pezkuwi-subxt"
|
||||
homepage = "https://pezkuwichain.org/"
|
||||
|
||||
[workspace.lints.rust]
|
||||
bad_style = "deny"
|
||||
improper_ctypes = "deny"
|
||||
missing_docs = "deny"
|
||||
non_shorthand_field_patterns = "deny"
|
||||
no_mangle_generic_items = "deny"
|
||||
overflowing_literals = "deny"
|
||||
path_statements = "deny"
|
||||
patterns_in_fns_without_body = "deny"
|
||||
unconditional_recursion = "deny"
|
||||
unused_allocation = "deny"
|
||||
unused_comparisons = "deny"
|
||||
unused_parens = "deny"
|
||||
unused_extern_crates = "deny"
|
||||
|
||||
[workspace.lints.clippy]
|
||||
type_complexity = "allow"
|
||||
# Priority -1 means that it can overwritten by other lints, https://rust-lang.github.io/rust-clippy/master/index.html#/lint_groups_priority
|
||||
all = { level = "deny", priority = -1 }
|
||||
|
||||
[workspace.dependencies]
|
||||
async-trait = "0.1.83"
|
||||
assert_matches = "1.5.0"
|
||||
base58 = { version = "0.2.0" }
|
||||
bitvec = { version = "1", default-features = false }
|
||||
blake2 = { version = "0.10.6", default-features = false }
|
||||
clap = { version = "4.5.18", features = ["derive", "cargo"] }
|
||||
cfg-if = "1.0.0"
|
||||
criterion = "0.5.1"
|
||||
codec = { package = "parity-scale-codec", version = "3.7.4", default-features = false }
|
||||
color-eyre = "0.6.3"
|
||||
console_error_panic_hook = "0.1.7"
|
||||
darling = "0.20.10"
|
||||
derive-where = "1.2.7"
|
||||
either = { version = "1.13.0", default-features = false }
|
||||
finito = { version = "0.1.0", default-features = false }
|
||||
frame-decode = { version = "0.15.0", default-features = false }
|
||||
frame-metadata = { version = "23.0.0", default-features = false }
|
||||
futures = { version = "0.3.31", default-features = false, features = ["std"] }
|
||||
getrandom = { version = "0.2", default-features = false }
|
||||
hashbrown = "0.14.5"
|
||||
hex = { version = "0.4.3", default-features = false, features = ["alloc"] }
|
||||
heck = "0.5.0"
|
||||
impl-serde = { version = "0.5.0", default-features = false }
|
||||
indoc = "2"
|
||||
jsonrpsee = { version = "0.24.5" }
|
||||
pretty_assertions = "1.4.1"
|
||||
primitive-types = { version = "0.13.1", default-features = false }
|
||||
proc-macro-error2 = "2.0.0"
|
||||
proc-macro2 = "1.0.86"
|
||||
quote = "1.0.37"
|
||||
regex = { version = "1.11.0", default-features = false }
|
||||
scale-info = { version = "2.11.4", default-features = false }
|
||||
scale-value = { version = "0.18.1", default-features = false }
|
||||
scale-bits = { version = "0.7.0", default-features = false }
|
||||
scale-decode = { version = "0.16.2", default-features = false }
|
||||
scale-encode = { version = "0.10.0", default-features = false }
|
||||
scale-type-resolver = { version = "0.2.0" }
|
||||
scale-info-legacy = { version = "0.4.0", default-features = false }
|
||||
scale-typegen = "0.12.0"
|
||||
scale-typegen-description = "0.11.0"
|
||||
serde = { version = "1.0.210", default-features = false, features = ["derive"] }
|
||||
serde_json = { version = "1.0.128", default-features = false }
|
||||
syn = { version = "2.0.77", features = ["full", "extra-traits"] }
|
||||
thiserror = { version = "2.0.0", default-features = false }
|
||||
tokio = { version = "1.44.2", default-features = false }
|
||||
tracing = { version = "0.1.40", default-features = false }
|
||||
tracing-wasm = "0.2.1"
|
||||
tracing-subscriber = { version = "0.3.20", features = ["env-filter"] }
|
||||
trybuild = "1.0.99"
|
||||
url = "2.5.2"
|
||||
wat = "1.228.0"
|
||||
wasm-bindgen-test = "0.3.24"
|
||||
which = "6.0.3"
|
||||
strip-ansi-escapes = "0.2.0"
|
||||
proptest = "1.5.0"
|
||||
hex-literal = "0.4.1"
|
||||
tower = "0.4"
|
||||
hyper = "1"
|
||||
http-body = "1"
|
||||
|
||||
# Light client support:
|
||||
smoldot = { version = "0.20.0", default-features = false }
|
||||
smoldot-light = { version = "0.18.0", default-features = false }
|
||||
tokio-stream = "0.1.16"
|
||||
futures-util = "0.3.31"
|
||||
rand = "0.8.5"
|
||||
pin-project = "1.1.5"
|
||||
|
||||
# Light client wasm:
|
||||
web-sys = { version = "0.3.70", features = ["BinaryType", "CloseEvent", "MessageEvent", "WebSocket"] }
|
||||
wasm-bindgen = "0.2.93"
|
||||
send_wrapper = "0.6.0"
|
||||
js-sys = "0.3.70"
|
||||
wasm-bindgen-futures = "0.4.43"
|
||||
futures-timer = "3"
|
||||
web-time = { version = "1.1", default-features = false }
|
||||
tokio-util = "0.7.12"
|
||||
|
||||
# Pezkuwi SDK crates (rebranded from Substrate) - using git deps:
|
||||
pezsc-executor = { git = "https://github.com/pezkuwichain/pezkuwi-sdk.git", branch = "main", default-features = false }
|
||||
pezsc-executor-common = { git = "https://github.com/pezkuwichain/pezkuwi-sdk.git", branch = "main", default-features = false }
|
||||
pezsp-crypto-hashing = { git = "https://github.com/pezkuwichain/pezkuwi-sdk.git", branch = "main", default-features = false }
|
||||
pezsp-core = { git = "https://github.com/pezkuwichain/pezkuwi-sdk.git", branch = "main", default-features = false }
|
||||
pezsp-keyring = { git = "https://github.com/pezkuwichain/pezkuwi-sdk.git", branch = "main", default-features = false }
|
||||
pezsp-maybe-compressed-blob = { git = "https://github.com/pezkuwichain/pezkuwi-sdk.git", branch = "main", default-features = false }
|
||||
pezsp-io = { git = "https://github.com/pezkuwichain/pezkuwi-sdk.git", branch = "main", default-features = false }
|
||||
pezsp-state-machine = { git = "https://github.com/pezkuwichain/pezkuwi-sdk.git", branch = "main", default-features = false }
|
||||
pezsp-runtime = { git = "https://github.com/pezkuwichain/pezkuwi-sdk.git", branch = "main", default-features = false }
|
||||
|
||||
# Pezkuwi-Subxt workspace crates:
|
||||
pezkuwi-subxt = { version = "0.44.0", path = "subxt", default-features = false }
|
||||
pezkuwi-subxt-core = { version = "0.44.0", path = "core", default-features = false }
|
||||
pezkuwi-subxt-macro = { version = "0.44.0", path = "macro" }
|
||||
pezkuwi-subxt-metadata = { version = "0.44.0", path = "metadata", default-features = false }
|
||||
pezkuwi-subxt-codegen = { version = "0.44.0", path = "codegen" }
|
||||
pezkuwi-subxt-signer = { version = "0.44.0", path = "signer", default-features = false }
|
||||
pezkuwi-subxt-rpcs = { version = "0.44.0", path = "rpcs", default-features = false }
|
||||
pezkuwi-subxt-lightclient = { version = "0.44.0", path = "lightclient", default-features = false }
|
||||
pezkuwi-subxt-utils-fetchmetadata = { version = "0.44.0", path = "utils/fetch-metadata", default-features = false }
|
||||
pezkuwi-subxt-utils-stripmetadata = { version = "0.44.0", path = "utils/strip-metadata", default-features = false }
|
||||
test-runtime = { path = "testing/test-runtime" }
|
||||
bizinikiwi-runner = { path = "testing/substrate-runner" }
|
||||
|
||||
# subxt-signer deps that I expect aren't useful anywhere else:
|
||||
bip39 = { version = "2.1.0", default-features = false }
|
||||
bip32 = { version = "0.5.2", default-features = false }
|
||||
hmac = { version = "0.12.1", default-features = false }
|
||||
pbkdf2 = { version = "0.12.2", default-features = false }
|
||||
schnorrkel = { version = "0.11.4", default-features = false }
|
||||
secp256k1 = { version = "0.30.0", default-features = false }
|
||||
keccak-hash = { version = "0.11.0", default-features = false }
|
||||
secrecy = "0.10.3"
|
||||
sha2 = { version = "0.10.8", default-features = false }
|
||||
zeroize = { version = "1", default-features = false }
|
||||
base64 = { version = "0.22.1", default-features = false }
|
||||
scrypt = { version = "0.11.0", default-features = false }
|
||||
crypto_secretbox = { version = "0.1.1", default-features = false }
|
||||
|
||||
[profile.dev.package.smoldot-light]
|
||||
opt-level = 2
|
||||
[profile.test.package.smoldot-light]
|
||||
opt-level = 2
|
||||
[profile.dev.package.smoldot]
|
||||
opt-level = 2
|
||||
[profile.test.package.smoldot]
|
||||
opt-level = 2
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
Vendored
+17
@@ -0,0 +1,17 @@
|
||||
Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of (at your option) either the Apache License,
|
||||
Version 2.0, or the GNU General Public License as published by the
|
||||
Free Software Foundation, either version 3 of the License, or (at your
|
||||
option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
|
||||
For details and specific language governing permissions and
|
||||
limitations, see either
|
||||
|
||||
- http://www.gnu.org/licenses/ for the GNU GPL
|
||||
- http://www.apache.org/licenses/LICENSE-2.0 for the Apache license
|
||||
Vendored
+79
@@ -0,0 +1,79 @@
|
||||
# subxt · [](https://github.com/paritytech/subxt/actions/workflows/rust.yml) [](https://crates.io/crates/subxt) [](https://docs.rs/subxt)
|
||||
|
||||
Subxt is a library for interacting with [Substrate](https://github.com/paritytech/polkadot-sdk) based nodes in Rust and WebAssembly. It can:
|
||||
|
||||
- Submit Extrinsics (this is where the name comes from).
|
||||
- Subscribe to blocks, reading the extrinsics and associated events from them.
|
||||
- Read and iterate over storage values.
|
||||
- Read constants and custom values from the metadata.
|
||||
- Call runtime APIs, returning the results.
|
||||
- Do all of the above via a safe, statically typed interface or via a dynamic one when you need the flexibility.
|
||||
- Compile to WASM and run entirely in the browser.
|
||||
- Do a bunch of things in a `#[no_std]` environment via the `subxt-core` crate.
|
||||
- Use a built-in light client (`smoldot`) to interact with chains.
|
||||
|
||||
## Usage
|
||||
|
||||
Take a look in the [examples](./subxt/examples) folder or the [examples](./examples) folder for various smaller or
|
||||
larger `subxt` usage examples, or [read the guide](https://docs.rs/subxt/latest/subxt/book/index.html) to learn more.
|
||||
|
||||
### Downloading metadata from a Substrate node
|
||||
|
||||
Use the [`subxt-cli`](./cli) tool to download the metadata for your target runtime from a node.
|
||||
|
||||
1. Install:
|
||||
|
||||
```bash
|
||||
cargo install subxt-cli
|
||||
```
|
||||
|
||||
2. Save the encoded metadata to a file:
|
||||
|
||||
```bash
|
||||
subxt metadata -f bytes > metadata.scale
|
||||
```
|
||||
|
||||
This defaults to querying the metadata of a locally running node on the default `http://localhost:9933/`. If querying
|
||||
a different node then the `metadata` command accepts a `--url` argument.
|
||||
|
||||
## Subxt Documentation
|
||||
|
||||
For more details regarding utilizing subxt, please visit the [documentation](https://docs.rs/subxt/latest/subxt/).
|
||||
|
||||
## Integration Testing
|
||||
|
||||
Most tests require a running substrate node to communicate with. This is done by spawning an instance of the
|
||||
substrate node per test. It requires an up-to-date `substrate` executable on your path.
|
||||
|
||||
This can be installed from source via cargo:
|
||||
|
||||
```bash
|
||||
cargo install --git https://github.com/paritytech/polkadot-sdk staging-node-cli --force
|
||||
```
|
||||
|
||||
## Real world usage
|
||||
|
||||
Please add your project to this list via a PR.
|
||||
|
||||
- [cargo-contract](https://github.com/paritytech/cargo-contract/) CLI for interacting with Wasm smart contracts.
|
||||
- [xcm-cli](https://github.com/ascjones/xcm-cli) CLI for submitting XCM messages.
|
||||
- [phala-pherry](https://github.com/Phala-Network/phala-blockchain/tree/master/standalone/pherry) The relayer between Phala blockchain and the off-chain Secure workers.
|
||||
- [crunch](https://github.com/turboflakes/crunch) CLI to claim staking rewards in batch every Era or X hours for substrate-based chains.
|
||||
- [interbtc-clients](https://github.com/interlay/interbtc-clients) Client implementations for the interBTC parachain; notably the Vault / Relayer and Oracle.
|
||||
- [tidext](https://github.com/tidelabs/tidext) Tidechain client with Stronghold signer.
|
||||
- [staking-miner-v2](https://github.com/paritytech/staking-miner-v2) Submit NPos election solutions and get rewards.
|
||||
- [polkadot-introspector](https://github.com/paritytech/polkadot-introspector) Tools for monitoring Polkadot nodes.
|
||||
- [ink!](https://github.com/paritytech/ink) Smart contract language that uses `subxt` for allowing developers to conduct [End-to-End testing](https://use.ink/basics/contract-testing/end-to-end-e2e-testing) of their contracts.
|
||||
- [Chainflip](https://github.com/chainflip-io/chainflip-backend) A decentralised exchange for native cross-chain swaps.
|
||||
- [Hyperbridge](https://github.com/polytope-labs/hyperbridge) A hyperscalable coprocessor for verifiable cross-chain interoperability.
|
||||
- [pop CLI](https://github.com/r0gue-io/pop-cli) The all-in-one tool for Polkadot development.
|
||||
|
||||
**Alternatives**
|
||||
|
||||
[substrate-api-client](https://github.com/scs/substrate-api-client) provides similar functionality.
|
||||
|
||||
#### License
|
||||
|
||||
The entire code within this repository is dual licensed under the _GPL-3.0_ or _Apache-2.0_ licenses. See [the LICENSE](./LICENSE) file for more details.
|
||||
|
||||
Please <a href="https://www.parity.io/contact/">contact us</a> if you have questions about the licensing of our products.
|
||||
Vendored
+108
@@ -0,0 +1,108 @@
|
||||
# Release Checklist
|
||||
|
||||
These steps assume that you've checked out the Subxt repository and are in the root directory of it.
|
||||
|
||||
We also assume that ongoing work done is being merged directly to the `master` branch.
|
||||
|
||||
1. Ensure that everything you'd like to see released is on the `master` branch.
|
||||
|
||||
2. Create a release branch off `master`, for example `release-v0.17.0`. Decide how far the version needs to be bumped based
|
||||
on the changes to date. If unsure what to bump the version to (e.g. is it a major, minor or patch release), check with the
|
||||
Parity Tools team.
|
||||
|
||||
3. Check that you're happy with the current documentation.
|
||||
|
||||
```
|
||||
cargo doc --open
|
||||
```
|
||||
|
||||
CI checks for broken internal links at the moment. Optionally you can also confirm that any external links
|
||||
are still valid like so:
|
||||
|
||||
```
|
||||
cargo install cargo-deadlinks
|
||||
cargo deadlinks --check-http
|
||||
```
|
||||
|
||||
If there are minor issues with the documentation, they can be fixed in the release branch.
|
||||
|
||||
4. Bump the crate versions in the root `Cargo.toml` to whatever was decided in step 2 (basically a find and replace from old version to new version in this file should do the trick).
|
||||
|
||||
5. Ensure the `Cargo.lock` file is up to date.
|
||||
|
||||
```
|
||||
cargo generate-lockfile
|
||||
```
|
||||
|
||||
6. Update `CHANGELOG.md` to reflect the difference between this release and the last. If you're unsure of
|
||||
what to add, check with the Tools team. See the `CHANGELOG.md` file for details of the format it follows.
|
||||
|
||||
First, if there have been any significant changes, add a description of those changes to the top of the
|
||||
changelog entry for this release.
|
||||
|
||||
Next, you can use the following script to generate the merged PRs between releases:
|
||||
|
||||
```
|
||||
./scripts/generate_changelog.sh
|
||||
```
|
||||
|
||||
Ensure that the script picked the latest published release tag (e.g. if releasing `v0.17.0`, the script should
|
||||
provide `[+] Latest release tag: v0.16.0` ). Then group the PRs into "Fixed", "Added" and "Changed" sections, and make any
|
||||
other adjustments that you feel are necessary for clarity.
|
||||
|
||||
7. If any of the differences impact the minimum version of `rustc` that the code will run on, please update the `rust-version`
|
||||
field in the root `Cargo.toml` accordingly.
|
||||
|
||||
8. Commit any of the above changes to the release branch and open a PR in GitHub with a base of `master`.
|
||||
|
||||
9. Once the branch has been reviewed and passes CI, merge it.
|
||||
|
||||
10. Now, we're ready to publish the release to crates.io.
|
||||
|
||||
1. Checkout `master`, ensuring we're looking at that latest merge (`git pull`).
|
||||
|
||||
```
|
||||
git checkout master && git pull
|
||||
```
|
||||
|
||||
2. Perform a final sanity check that everything looks ok.
|
||||
|
||||
```
|
||||
cargo test --all-targets
|
||||
```
|
||||
|
||||
3. Run the following command to publish each crate in the required order (allowing
|
||||
a little time in between each to let crates.io catch up with what we've published).
|
||||
|
||||
```
|
||||
(cd utils/strip-metadata && cargo publish) && \
|
||||
(cd metadata && cargo publish) && \
|
||||
(cd lightclient && cargo publish) && \
|
||||
(cd utils/fetch-metadata && cargo publish) && \
|
||||
(cd codegen && cargo publish) && \
|
||||
(cd macro && cargo publish);
|
||||
```
|
||||
|
||||
Now, remove the dev dependencies from `subxt-core` (to avoid circular deps), and then run:
|
||||
|
||||
```
|
||||
(cd core && cargo publish) && \
|
||||
(cd rpcs && cargo publish) && \
|
||||
(cd subxt && cargo publish) && \
|
||||
(cd signer && cargo publish) && \
|
||||
(cd cli && cargo publish);
|
||||
```
|
||||
|
||||
Finally, put back the dev dependencies in `subxt-core`.
|
||||
|
||||
11. If the release was successful, tag the commit that we released in the `master` branch with the
|
||||
version that we just released, for example:
|
||||
|
||||
```
|
||||
git tag -s v0.17.0 # use the version number you've just published to crates.io, not this one
|
||||
git push --tags
|
||||
```
|
||||
|
||||
Once this is pushed, go along to [the releases page on GitHub](https://github.com/paritytech/subxt/releases)
|
||||
and draft a new release which points to the tag you just pushed to `master` above. Copy the changelog comments
|
||||
for the current release into the release description.
|
||||
Vendored
+58
@@ -0,0 +1,58 @@
|
||||
[package]
|
||||
name = "pezkuwi-subxt-cli"
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
publish = true
|
||||
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
documentation = "https://docs.rs/subxt-cli"
|
||||
homepage.workspace = true
|
||||
description = "Command line utilities for working with subxt codegen"
|
||||
|
||||
[[bin]]
|
||||
name = "subxt"
|
||||
path = "src/main.rs"
|
||||
doc = false
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[features]
|
||||
# Compute the state root hash from the genesis entry.
|
||||
# Enable this to create a smaller chain spec file.
|
||||
chain-spec-pruning = ["smoldot"]
|
||||
|
||||
[dependencies]
|
||||
pezkuwi-subxt-codegen = { workspace = true }
|
||||
scale-typegen = { workspace = true }
|
||||
pezkuwi-subxt-utils-fetchmetadata = { workspace = true, features = ["url"] }
|
||||
pezkuwi-subxt-utils-stripmetadata = { workspace = true }
|
||||
pezkuwi-subxt-metadata = { workspace = true, features = ["legacy"] }
|
||||
pezkuwi-subxt = { workspace = true, features = ["default"] }
|
||||
clap = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
color-eyre = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
hex = { workspace = true }
|
||||
frame-decode = { workspace = true, features = ["legacy-types"] }
|
||||
frame-metadata = { workspace = true }
|
||||
codec = { package = "parity-scale-codec", workspace = true }
|
||||
scale-info = { workspace = true }
|
||||
scale-info-legacy = { workspace = true }
|
||||
scale-value = { workspace = true }
|
||||
syn = { workspace = true }
|
||||
quote = { workspace = true }
|
||||
jsonrpsee = { workspace = true, features = ["async-client", "client-ws-transport-tls", "http-client"] }
|
||||
tokio = { workspace = true, features = ["rt-multi-thread"] }
|
||||
scale-typegen-description = { workspace = true }
|
||||
heck = { workspace = true }
|
||||
indoc = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
smoldot = { workspace = true, optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
strip-ansi-escapes = { workspace = true }
|
||||
pretty_assertions = { workspace = true }
|
||||
Vendored
+58
@@ -0,0 +1,58 @@
|
||||
# subxt-cli
|
||||
|
||||
Utilities for working with substrate metadata for `subxt`
|
||||
|
||||
```
|
||||
USAGE:
|
||||
subxt <SUBCOMMAND>
|
||||
|
||||
FLAGS:
|
||||
-h, --help
|
||||
Prints help information
|
||||
|
||||
-V, --version
|
||||
Prints version information
|
||||
|
||||
|
||||
SUBCOMMANDS:
|
||||
codegen Generate runtime API client code from metadata
|
||||
help Prints this message or the help of the given subcommand(s)
|
||||
metadata Download metadata from a substrate node, for use with `subxt` codegen
|
||||
```
|
||||
|
||||
## Metadata
|
||||
|
||||
Use to download metadata for inspection, or use in the `subxt` macro. e.g.
|
||||
|
||||
`subxt metadata -f bytes > metadata.scale`
|
||||
|
||||
```
|
||||
USAGE:
|
||||
subxt metadata [OPTIONS]
|
||||
|
||||
OPTIONS:
|
||||
-f, --format <format> the format of the metadata to display: `json`, `hex` or `bytes` [default: json]
|
||||
--url <url> the url of the substrate node to query for metadata [default: http://localhost:9933]
|
||||
```
|
||||
|
||||
## Codegen
|
||||
|
||||
Use to invoke the `subxt-codegen` crate which is used by `subxt-macro` to generate the runtime API and types. Useful
|
||||
for troubleshooting codegen as an alternative to `cargo expand`, and also provides the possibility to customize the
|
||||
generated code if the macro does not produce the desired API. e.g.
|
||||
|
||||
`subxt codegen | rustfmt --edition=2018 --emit=stdout`
|
||||
|
||||
```
|
||||
USAGE:
|
||||
subxt codegen [OPTIONS]
|
||||
|
||||
OPTIONS:
|
||||
-f, --file <file>
|
||||
the path to the encoded metadata file
|
||||
|
||||
--url <url>
|
||||
the url of the substrate node to query for metadata for codegen
|
||||
|
||||
```
|
||||
|
||||
Vendored
+34
@@ -0,0 +1,34 @@
|
||||
//! Build script for the CLI.
|
||||
|
||||
use std::{borrow::Cow, process::Command};
|
||||
|
||||
fn main() {
|
||||
// Make git hash available via GIT_HASH build-time env var:
|
||||
output_git_short_hash();
|
||||
}
|
||||
|
||||
fn output_git_short_hash() {
|
||||
let output = Command::new("git")
|
||||
.args(["rev-parse", "--short=11", "HEAD"])
|
||||
.output();
|
||||
|
||||
let git_hash = match output {
|
||||
Ok(o) if o.status.success() => {
|
||||
let sha = String::from_utf8_lossy(&o.stdout).trim().to_owned();
|
||||
Cow::from(sha)
|
||||
}
|
||||
Ok(o) => {
|
||||
println!("cargo:warning=Git command failed with status: {}", o.status);
|
||||
Cow::from("unknown")
|
||||
}
|
||||
Err(err) => {
|
||||
println!("cargo:warning=Failed to execute git command: {err}");
|
||||
Cow::from("unknown")
|
||||
}
|
||||
};
|
||||
|
||||
println!("cargo:rustc-env=GIT_HASH={git_hash}");
|
||||
println!("cargo:rerun-if-changed=../.git/HEAD");
|
||||
println!("cargo:rerun-if-changed=../.git/refs");
|
||||
println!("cargo:rerun-if-changed=build.rs");
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
use jsonrpsee::{
|
||||
async_client::ClientBuilder,
|
||||
client_transport::ws::WsTransportClientBuilder,
|
||||
core::client::{ClientT, Error},
|
||||
http_client::HttpClientBuilder,
|
||||
};
|
||||
use std::time::Duration;
|
||||
use pezkuwi_subxt_utils_fetchmetadata::Url;
|
||||
|
||||
/// Returns the node's chainSpec from the provided URL.
|
||||
pub async fn fetch_chain_spec(url: Url) -> Result<serde_json::Value, FetchSpecError> {
|
||||
async fn fetch_ws(url: Url) -> Result<serde_json::Value, Error> {
|
||||
let (sender, receiver) = WsTransportClientBuilder::default()
|
||||
.build(url)
|
||||
.await
|
||||
.map_err(|e| Error::Transport(e.into()))?;
|
||||
|
||||
let client = ClientBuilder::default()
|
||||
.request_timeout(Duration::from_secs(180))
|
||||
.max_buffer_capacity_per_subscription(4096)
|
||||
.build_with_tokio(sender, receiver);
|
||||
|
||||
inner_fetch(client).await
|
||||
}
|
||||
|
||||
async fn fetch_http(url: Url) -> Result<serde_json::Value, Error> {
|
||||
let client = HttpClientBuilder::default()
|
||||
.request_timeout(Duration::from_secs(180))
|
||||
.build(url)?;
|
||||
|
||||
inner_fetch(client).await
|
||||
}
|
||||
|
||||
async fn inner_fetch(client: impl ClientT) -> Result<serde_json::Value, Error> {
|
||||
client
|
||||
.request("sync_state_genSyncSpec", jsonrpsee::rpc_params![true])
|
||||
.await
|
||||
}
|
||||
|
||||
let spec = match url.scheme() {
|
||||
"http" | "https" => fetch_http(url).await.map_err(FetchSpecError::RequestError),
|
||||
"ws" | "wss" => fetch_ws(url).await.map_err(FetchSpecError::RequestError),
|
||||
invalid_scheme => Err(FetchSpecError::InvalidScheme(invalid_scheme.to_owned())),
|
||||
}?;
|
||||
|
||||
Ok(spec)
|
||||
}
|
||||
|
||||
/// Error attempting to fetch chainSpec.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[non_exhaustive]
|
||||
pub enum FetchSpecError {
|
||||
/// JSON-RPC error fetching metadata.
|
||||
#[error("Request error: {0}")]
|
||||
RequestError(#[from] jsonrpsee::core::ClientError),
|
||||
/// URL scheme is not http, https, ws or wss.
|
||||
#[error("'{0}' not supported, supported URI schemes are http, https, ws or wss.")]
|
||||
InvalidScheme(String),
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
use clap::Parser as ClapParser;
|
||||
#[cfg(feature = "chain-spec-pruning")]
|
||||
use serde_json::Value;
|
||||
use std::{io::Write, path::PathBuf};
|
||||
use pezkuwi_subxt_utils_fetchmetadata::Url;
|
||||
|
||||
mod fetch;
|
||||
|
||||
/// Download chainSpec from a substrate node.
|
||||
#[derive(Debug, ClapParser)]
|
||||
pub struct Opts {
|
||||
/// The url of the substrate node to query for metadata for codegen.
|
||||
#[clap(long)]
|
||||
url: Url,
|
||||
/// Write the output of the command to the provided file path.
|
||||
#[clap(long, short, value_parser)]
|
||||
output_file: Option<PathBuf>,
|
||||
/// Replaced the genesis raw entry with a stateRootHash to optimize
|
||||
/// the spec size and avoid the need to calculate the genesis storage.
|
||||
///
|
||||
/// This option is enabled with the `chain-spec-pruning` feature.
|
||||
///
|
||||
/// Defaults to `false`.
|
||||
#[cfg(feature = "chain-spec-pruning")]
|
||||
#[clap(long)]
|
||||
state_root_hash: bool,
|
||||
/// Remove the `codeSubstitutes` entry from the chain spec.
|
||||
/// This is useful when wanting to store a smaller chain spec.
|
||||
/// At this moment, the light client does not utilize this object.
|
||||
///
|
||||
/// Defaults to `false`.
|
||||
#[clap(long)]
|
||||
remove_substitutes: bool,
|
||||
}
|
||||
|
||||
/// Error attempting to fetch chainSpec.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[non_exhaustive]
|
||||
pub enum ChainSpecError {
|
||||
/// Failed to fetch the chain spec.
|
||||
#[error("Failed to fetch the chain spec: {0}")]
|
||||
FetchError(#[from] fetch::FetchSpecError),
|
||||
|
||||
#[cfg(feature = "chain-spec-pruning")]
|
||||
/// The provided chain spec is invalid.
|
||||
#[error("Error while parsing the chain spec: {0})")]
|
||||
ParseError(String),
|
||||
|
||||
#[cfg(feature = "chain-spec-pruning")]
|
||||
/// Cannot compute the state root hash.
|
||||
#[error("Error computing state root hash: {0})")]
|
||||
ComputeError(String),
|
||||
|
||||
/// Other error.
|
||||
#[error("Other: {0})")]
|
||||
Other(String),
|
||||
}
|
||||
|
||||
#[cfg(feature = "chain-spec-pruning")]
|
||||
fn compute_state_root_hash(spec: &Value) -> Result<[u8; 32], ChainSpecError> {
|
||||
let chain_spec = smoldot::chain_spec::ChainSpec::from_json_bytes(spec.to_string().as_bytes())
|
||||
.map_err(|err| ChainSpecError::ParseError(err.to_string()))?;
|
||||
|
||||
let genesis_chain_information = chain_spec.to_chain_information().map(|(ci, _)| ci);
|
||||
|
||||
let state_root = match genesis_chain_information {
|
||||
Ok(genesis_chain_information) => {
|
||||
let header = genesis_chain_information.as_ref().finalized_block_header;
|
||||
*header.state_root
|
||||
}
|
||||
// From the smoldot code this error is encountered when the genesis already contains the
|
||||
// state root hash entry instead of the raw entry.
|
||||
Err(smoldot::chain_spec::FromGenesisStorageError::UnknownStorageItems) => *chain_spec
|
||||
.genesis_storage()
|
||||
.into_trie_root_hash()
|
||||
.ok_or_else(|| {
|
||||
ChainSpecError::ParseError(
|
||||
"The chain spec does not contain the proper shape for the genesis.raw entry"
|
||||
.to_string(),
|
||||
)
|
||||
})?,
|
||||
Err(err) => return Err(ChainSpecError::ComputeError(err.to_string())),
|
||||
};
|
||||
|
||||
Ok(state_root)
|
||||
}
|
||||
|
||||
pub async fn run(opts: Opts, output: &mut impl Write) -> color_eyre::Result<()> {
|
||||
let url = opts.url;
|
||||
|
||||
let mut spec = fetch::fetch_chain_spec(url).await?;
|
||||
|
||||
let mut output: Box<dyn Write> = match opts.output_file {
|
||||
Some(path) => Box::new(std::fs::File::create(path)?),
|
||||
None => Box::new(output),
|
||||
};
|
||||
|
||||
#[cfg(feature = "chain-spec-pruning")]
|
||||
if opts.state_root_hash {
|
||||
let state_root_hash = compute_state_root_hash(&spec)?;
|
||||
let state_root_hash = format!("0x{}", hex::encode(state_root_hash));
|
||||
|
||||
if let Some(genesis) = spec.get_mut("genesis") {
|
||||
let object = genesis.as_object_mut().ok_or_else(|| {
|
||||
ChainSpecError::Other("The genesis entry must be an object".to_string())
|
||||
})?;
|
||||
|
||||
object.remove("raw").ok_or_else(|| {
|
||||
ChainSpecError::Other("The genesis entry must contain a raw entry".to_string())
|
||||
})?;
|
||||
|
||||
object.insert("stateRootHash".to_string(), Value::String(state_root_hash));
|
||||
}
|
||||
}
|
||||
|
||||
if opts.remove_substitutes {
|
||||
let object = spec
|
||||
.as_object_mut()
|
||||
.ok_or_else(|| ChainSpecError::Other("The chain spec must be an object".to_string()))?;
|
||||
|
||||
object.remove("codeSubstitutes");
|
||||
}
|
||||
|
||||
let json = serde_json::to_string_pretty(&spec)?;
|
||||
write!(output, "{json}")?;
|
||||
Ok(())
|
||||
}
|
||||
+470
@@ -0,0 +1,470 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
use crate::utils::{FileOrUrl, validate_url_security};
|
||||
use clap::Parser as ClapParser;
|
||||
use color_eyre::eyre::eyre;
|
||||
use scale_typegen_description::scale_typegen::typegen::{
|
||||
settings::substitutes::path_segments,
|
||||
validation::{registry_contains_type_path, similar_type_paths_in_registry},
|
||||
};
|
||||
use std::path::PathBuf;
|
||||
use pezkuwi_subxt_codegen::CodegenBuilder;
|
||||
use pezkuwi_subxt_metadata::Metadata;
|
||||
|
||||
/// Generate runtime API client code from metadata.
|
||||
///
|
||||
/// # Example (with code formatting)
|
||||
///
|
||||
/// `subxt codegen | rustfmt --edition=2018 --emit=stdout`
|
||||
#[derive(Debug, ClapParser)]
|
||||
pub struct Opts {
|
||||
#[command(flatten)]
|
||||
file_or_url: FileOrUrl,
|
||||
/// Additional derives
|
||||
#[clap(long = "derive")]
|
||||
derives: Vec<String>,
|
||||
/// Additional attributes
|
||||
#[clap(long = "attribute")]
|
||||
attributes: Vec<String>,
|
||||
/// Path to legacy type definitions (required for metadatas pre-V14)
|
||||
#[clap(long)]
|
||||
legacy_types: Option<PathBuf>,
|
||||
/// The spec version of the legacy metadata (required for metadatas pre-V14)
|
||||
#[clap(long)]
|
||||
legacy_spec_version: Option<u64>,
|
||||
/// Additional derives for a given type.
|
||||
///
|
||||
/// Example 1: `--derive-for-type my_module::my_type=serde::Serialize`.
|
||||
/// Example 2: `--derive-for-type my_module::my_type=serde::Serialize,recursive`.
|
||||
#[clap(long = "derive-for-type", value_parser = derive_for_type_parser)]
|
||||
derives_for_type: Vec<DeriveForType>,
|
||||
/// Additional attributes for a given type.
|
||||
///
|
||||
/// Example 1: `--attributes-for-type my_module::my_type=#[allow(clippy::all)]`.
|
||||
/// Example 2: `--attributes-for-type my_module::my_type=#[allow(clippy::all)],recursive`.
|
||||
#[clap(long = "attributes-for-type", value_parser = attributes_for_type_parser)]
|
||||
attributes_for_type: Vec<AttributeForType>,
|
||||
/// Substitute a type for another.
|
||||
///
|
||||
/// Example `--substitute-type sp_runtime::MultiAddress<A,B>=subxt::utils::Static<::sp_runtime::MultiAddress<A,B>>`
|
||||
#[clap(long = "substitute-type", value_parser = substitute_type_parser)]
|
||||
substitute_types: Vec<(String, String)>,
|
||||
/// The `subxt` crate access path in the generated code.
|
||||
/// Defaults to `::pezkuwi_subxt::ext::pezkuwi_subxt_core`.
|
||||
#[clap(long = "crate")]
|
||||
crate_path: Option<String>,
|
||||
/// Do not generate documentation for the runtime API code.
|
||||
///
|
||||
/// Defaults to `false` (documentation is generated).
|
||||
#[clap(long, action)]
|
||||
no_docs: bool,
|
||||
/// Whether to limit code generation to only runtime types.
|
||||
///
|
||||
/// Defaults to `false` (all types are generated).
|
||||
#[clap(long)]
|
||||
runtime_types_only: bool,
|
||||
/// Do not provide default trait derivations for the generated types.
|
||||
///
|
||||
/// Defaults to `false` (default trait derivations are provided).
|
||||
#[clap(long)]
|
||||
no_default_derives: bool,
|
||||
/// Do not provide default substitutions for the generated types.
|
||||
///
|
||||
/// Defaults to `false` (default substitutions are provided).
|
||||
#[clap(long)]
|
||||
no_default_substitutions: bool,
|
||||
/// Allow insecure URLs e.g. URLs starting with ws:// or http:// without SSL encryption
|
||||
#[clap(long, short)]
|
||||
allow_insecure: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct DeriveForType {
|
||||
type_path: String,
|
||||
trait_path: String,
|
||||
recursive: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct AttributeForType {
|
||||
type_path: String,
|
||||
attribute: String,
|
||||
recursive: bool,
|
||||
}
|
||||
|
||||
fn derive_for_type_parser(src: &str) -> Result<DeriveForType, String> {
|
||||
let (type_path, trait_path, recursive) = type_map_parser(src)
|
||||
.ok_or_else(|| String::from("Invalid pattern for `derive-for-type`. It should be `type=derive` or `type=derive,recursive`, like `my_type=serde::Serialize` or `my_type=serde::Serialize,recursive`"))?;
|
||||
Ok(DeriveForType {
|
||||
type_path: type_path.to_string(),
|
||||
trait_path: trait_path.to_string(),
|
||||
recursive,
|
||||
})
|
||||
}
|
||||
|
||||
fn attributes_for_type_parser(src: &str) -> Result<AttributeForType, String> {
|
||||
let (type_path, attribute, recursive) = type_map_parser(src)
|
||||
.ok_or_else(|| String::from("Invalid pattern for `attributes-for-type`. It should be `type=attribute` like `my_type=serde::#[allow(clippy::all)]` or `type=attribute,recursive` like `my_type=serde::#[allow(clippy::all)],recursive`"))?;
|
||||
Ok(AttributeForType {
|
||||
type_path: type_path.to_string(),
|
||||
attribute: attribute.to_string(),
|
||||
recursive,
|
||||
})
|
||||
}
|
||||
|
||||
/// Parses a `&str` of the form `str1=str2` into `(str1, str2, false)` or `str1=str2,recursive` into `(str1, str2, true)`.
|
||||
///
|
||||
/// A `None` value returned is a parsing error.
|
||||
fn type_map_parser(src: &str) -> Option<(&str, &str, bool)> {
|
||||
let (str1, rest) = src.split_once('=')?;
|
||||
|
||||
let mut split_rest = rest.split(',');
|
||||
let str2 = split_rest
|
||||
.next()
|
||||
.expect("split iter always returns at least one element; qed");
|
||||
|
||||
let mut recursive = false;
|
||||
for r in split_rest {
|
||||
match r {
|
||||
// Note: later we can add other attributes to this match
|
||||
"recursive" => {
|
||||
recursive = true;
|
||||
}
|
||||
_ => return None,
|
||||
}
|
||||
}
|
||||
|
||||
Some((str1, str2, recursive))
|
||||
}
|
||||
|
||||
fn substitute_type_parser(src: &str) -> Result<(String, String), String> {
|
||||
let (from, to) = src
|
||||
.split_once('=')
|
||||
.ok_or_else(|| String::from("Invalid pattern for `substitute-type`. It should be something like `input::Type<A>=replacement::Type<A>`"))?;
|
||||
|
||||
Ok((from.to_string(), to.to_string()))
|
||||
}
|
||||
|
||||
pub async fn run(opts: Opts, output: &mut impl std::io::Write) -> color_eyre::Result<()> {
|
||||
validate_url_security(opts.file_or_url.url.as_ref(), opts.allow_insecure)?;
|
||||
|
||||
let bytes = opts.file_or_url.fetch().await?;
|
||||
let legacy_types = opts
|
||||
.legacy_types
|
||||
.map(|path| {
|
||||
let bytes = std::fs::read(path).map_err(|e| eyre!("Cannot read legacy_types: {e}"))?;
|
||||
let types = frame_decode::legacy_types::from_bytes(&bytes)
|
||||
.map_err(|e| eyre!("Cannot deserialize legacy_types: {e}"))?;
|
||||
Ok::<_, color_eyre::eyre::Error>(types)
|
||||
})
|
||||
.transpose()?;
|
||||
|
||||
codegen(
|
||||
&bytes,
|
||||
legacy_types,
|
||||
opts.legacy_spec_version,
|
||||
opts.derives,
|
||||
opts.attributes,
|
||||
opts.derives_for_type,
|
||||
opts.attributes_for_type,
|
||||
opts.substitute_types,
|
||||
opts.crate_path,
|
||||
opts.no_docs,
|
||||
opts.runtime_types_only,
|
||||
opts.no_default_derives,
|
||||
opts.no_default_substitutions,
|
||||
output,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct OuterAttribute(syn::Attribute);
|
||||
|
||||
impl syn::parse::Parse for OuterAttribute {
|
||||
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
|
||||
Ok(Self(input.call(syn::Attribute::parse_outer)?[0].clone()))
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn codegen(
|
||||
metadata_bytes: &[u8],
|
||||
legacy_types: Option<scale_info_legacy::ChainTypeRegistry>,
|
||||
legacy_spec_version: Option<u64>,
|
||||
raw_derives: Vec<String>,
|
||||
raw_attributes: Vec<String>,
|
||||
derives_for_type: Vec<DeriveForType>,
|
||||
attributes_for_type: Vec<AttributeForType>,
|
||||
substitute_types: Vec<(String, String)>,
|
||||
crate_path: Option<String>,
|
||||
no_docs: bool,
|
||||
runtime_types_only: bool,
|
||||
no_default_derives: bool,
|
||||
no_default_substitutions: bool,
|
||||
output: &mut impl std::io::Write,
|
||||
) -> color_eyre::Result<()> {
|
||||
let mut codegen = CodegenBuilder::new();
|
||||
|
||||
// Use the provided crate path:
|
||||
if let Some(crate_path) = crate_path {
|
||||
let crate_path =
|
||||
syn::parse_str(&crate_path).map_err(|e| eyre!("Cannot parse crate path: {e}"))?;
|
||||
codegen.set_subxt_crate_path(crate_path);
|
||||
}
|
||||
|
||||
// Respect the boolean flags:
|
||||
if runtime_types_only {
|
||||
codegen.runtime_types_only()
|
||||
}
|
||||
if no_default_derives {
|
||||
codegen.disable_default_derives()
|
||||
}
|
||||
if no_default_substitutions {
|
||||
codegen.disable_default_substitutes()
|
||||
}
|
||||
if no_docs {
|
||||
codegen.no_docs()
|
||||
}
|
||||
|
||||
let metadata = {
|
||||
let runtime_metadata = pezkuwi_subxt_metadata::decode_runtime_metadata(metadata_bytes)?;
|
||||
let mut metadata = match runtime_metadata {
|
||||
// Too old to work with:
|
||||
frame_metadata::RuntimeMetadata::V0(_)
|
||||
| frame_metadata::RuntimeMetadata::V1(_)
|
||||
| frame_metadata::RuntimeMetadata::V2(_)
|
||||
| frame_metadata::RuntimeMetadata::V3(_)
|
||||
| frame_metadata::RuntimeMetadata::V4(_)
|
||||
| frame_metadata::RuntimeMetadata::V5(_)
|
||||
| frame_metadata::RuntimeMetadata::V6(_)
|
||||
| frame_metadata::RuntimeMetadata::V7(_) => {
|
||||
Err(eyre!("Metadata V1-V7 cannot be decoded from"))
|
||||
}
|
||||
// Converting legacy metadatas:
|
||||
frame_metadata::RuntimeMetadata::V8(md) => {
|
||||
let legacy_types = legacy_types
|
||||
.ok_or_else(|| eyre!("--legacy-types needed to load V8 metadata"))?;
|
||||
let legacy_spec = legacy_spec_version
|
||||
.ok_or_else(|| eyre!("--legacy-spec-version needed to load V8 metadata"))?;
|
||||
Metadata::from_v8(&md, &legacy_types.for_spec_version(legacy_spec))
|
||||
.map_err(|e| eyre!("Cannot load V8 metadata: {e}"))
|
||||
}
|
||||
frame_metadata::RuntimeMetadata::V9(md) => {
|
||||
let legacy_types = legacy_types
|
||||
.ok_or_else(|| eyre!("--legacy-types needed to load V9 metadata"))?;
|
||||
let legacy_spec = legacy_spec_version
|
||||
.ok_or_else(|| eyre!("--legacy-spec-version needed to load V9 metadata"))?;
|
||||
Metadata::from_v9(&md, &legacy_types.for_spec_version(legacy_spec))
|
||||
.map_err(|e| eyre!("Cannot load V9 metadata: {e}"))
|
||||
}
|
||||
frame_metadata::RuntimeMetadata::V10(md) => {
|
||||
let legacy_types = legacy_types
|
||||
.ok_or_else(|| eyre!("--legacy-types needed to load V10 metadata"))?;
|
||||
let legacy_spec = legacy_spec_version
|
||||
.ok_or_else(|| eyre!("--legacy-spec-version needed to load V10 metadata"))?;
|
||||
Metadata::from_v10(&md, &legacy_types.for_spec_version(legacy_spec))
|
||||
.map_err(|e| eyre!("Cannot load V10 metadata: {e}"))
|
||||
}
|
||||
frame_metadata::RuntimeMetadata::V11(md) => {
|
||||
let legacy_types = legacy_types
|
||||
.ok_or_else(|| eyre!("--legacy-types needed to load V11 metadata"))?;
|
||||
let legacy_spec = legacy_spec_version
|
||||
.ok_or_else(|| eyre!("--legacy-spec-version needed to load V11 metadata"))?;
|
||||
Metadata::from_v11(&md, &legacy_types.for_spec_version(legacy_spec))
|
||||
.map_err(|e| eyre!("Cannot load V11 metadata: {e}"))
|
||||
}
|
||||
frame_metadata::RuntimeMetadata::V12(md) => {
|
||||
let legacy_types = legacy_types
|
||||
.ok_or_else(|| eyre!("--legacy-types needed to load V12 metadata"))?;
|
||||
let legacy_spec = legacy_spec_version
|
||||
.ok_or_else(|| eyre!("--legacy-spec-version needed to load V12 metadata"))?;
|
||||
Metadata::from_v12(&md, &legacy_types.for_spec_version(legacy_spec))
|
||||
.map_err(|e| eyre!("Cannot load V12 metadata: {e}"))
|
||||
}
|
||||
frame_metadata::RuntimeMetadata::V13(md) => {
|
||||
let legacy_types = legacy_types
|
||||
.ok_or_else(|| eyre!("--legacy-types needed to load V13 metadata"))?;
|
||||
let legacy_spec = legacy_spec_version
|
||||
.ok_or_else(|| eyre!("--legacy-spec-version needed to load V13 metadata"))?;
|
||||
Metadata::from_v13(&md, &legacy_types.for_spec_version(legacy_spec))
|
||||
.map_err(|e| eyre!("Cannot load V13 metadata: {e}"))
|
||||
}
|
||||
// Converting modern metadatas:
|
||||
frame_metadata::RuntimeMetadata::V14(md) => {
|
||||
Metadata::from_v14(md).map_err(|e| eyre!("Cannot load V14 metadata: {e}"))
|
||||
}
|
||||
frame_metadata::RuntimeMetadata::V15(md) => {
|
||||
Metadata::from_v15(md).map_err(|e| eyre!("Cannot load V15 metadata: {e}"))
|
||||
}
|
||||
frame_metadata::RuntimeMetadata::V16(md) => {
|
||||
Metadata::from_v16(md).map_err(|e| eyre!("Cannot load V16 metadata: {e}"))
|
||||
}
|
||||
}?;
|
||||
|
||||
// Run this first to ensure type paths are unique (which may result in 1,2,3 suffixes being added
|
||||
// to type paths), so that when we validate derives/substitutions below, they are allowed for such
|
||||
// types. See <https://github.com/paritytech/subxt/issues/2011>.
|
||||
scale_typegen::utils::ensure_unique_type_paths(metadata.types_mut())
|
||||
.expect("ensure_unique_type_paths should not fail; please report an issue.");
|
||||
|
||||
metadata
|
||||
};
|
||||
|
||||
// Configure derives:
|
||||
let global_derives = raw_derives
|
||||
.iter()
|
||||
.map(|raw| syn::parse_str(raw))
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|e| eyre!("Cannot parse global derives: {e}"))?;
|
||||
codegen.set_additional_global_derives(global_derives);
|
||||
|
||||
for d in derives_for_type {
|
||||
let ty_str = &d.type_path;
|
||||
let ty: syn::TypePath = syn::parse_str(ty_str)
|
||||
.map_err(|e| eyre!("Cannot parse derive for type {ty_str}: {e}"))?;
|
||||
let derive = syn::parse_str(&d.trait_path)
|
||||
.map_err(|e| eyre!("Cannot parse derive for type {ty_str}: {e}"))?;
|
||||
|
||||
validate_path_with_metadata(&ty.path, &metadata)?;
|
||||
// Note: recursive derives and attributes not supported in the CLI => recursive: false
|
||||
codegen.add_derives_for_type(ty, std::iter::once(derive), d.recursive);
|
||||
}
|
||||
|
||||
// Configure attributes:
|
||||
let universal_attributes = raw_attributes
|
||||
.iter()
|
||||
.map(|raw| syn::parse_str(raw))
|
||||
.map(|attr: syn::Result<OuterAttribute>| attr.map(|attr| attr.0))
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|e| eyre!("Cannot parse global attributes: {e}"))?;
|
||||
codegen.set_additional_global_attributes(universal_attributes);
|
||||
|
||||
for a in attributes_for_type {
|
||||
let ty_str = &a.type_path;
|
||||
let ty: syn::TypePath = syn::parse_str(ty_str)
|
||||
.map_err(|e| eyre!("Cannot parse attribute for type {ty_str}: {e}"))?;
|
||||
let attribute: OuterAttribute = syn::parse_str(&a.attribute)
|
||||
.map_err(|e| eyre!("Cannot parse attribute for type {ty_str}: {e}"))?;
|
||||
|
||||
validate_path_with_metadata(&ty.path, &metadata)?;
|
||||
// Note: recursive derives and attributes not supported in the CLI => recursive: false
|
||||
codegen.add_attributes_for_type(ty, std::iter::once(attribute.0), a.recursive);
|
||||
}
|
||||
|
||||
// Insert type substitutions:
|
||||
for (from_str, to_str) in substitute_types {
|
||||
let from: syn::Path = syn::parse_str(&from_str)
|
||||
.map_err(|e| eyre!("Cannot parse type substitution for path {from_str}: {e}"))?;
|
||||
let to: syn::Path = syn::parse_str(&to_str)
|
||||
.map_err(|e| eyre!("Cannot parse type substitution for path {from_str}: {e}"))?;
|
||||
|
||||
validate_path_with_metadata(&from, &metadata)?;
|
||||
codegen.set_type_substitute(from, to);
|
||||
}
|
||||
|
||||
let code = codegen
|
||||
.generate(metadata)
|
||||
.map_err(|e| eyre!("Cannot generate code: {e}"))?;
|
||||
|
||||
writeln!(output, "{code}")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Validates that the type path is part of the metadata.
|
||||
fn validate_path_with_metadata(path: &syn::Path, metadata: &Metadata) -> color_eyre::Result<()> {
|
||||
fn pretty_path(path: &syn::Path) -> String {
|
||||
use quote::ToTokens;
|
||||
path.to_token_stream().to_string().replace(' ', "")
|
||||
}
|
||||
|
||||
let path_segments = path_segments(path);
|
||||
let ident = &path
|
||||
.segments
|
||||
.last()
|
||||
.expect("Empty path should be filtered out before already")
|
||||
.ident;
|
||||
if !registry_contains_type_path(metadata.types(), &path_segments) {
|
||||
let alternatives = similar_type_paths_in_registry(metadata.types(), path);
|
||||
let alternatives: String = if alternatives.is_empty() {
|
||||
format!("There is no Type with name `{ident}` in the provided metadata.")
|
||||
} else {
|
||||
let mut s = "A type with the same name is present at: ".to_owned();
|
||||
for p in alternatives {
|
||||
s.push('\n');
|
||||
s.push_str(&pretty_path(&p));
|
||||
}
|
||||
s
|
||||
};
|
||||
|
||||
color_eyre::eyre::bail!(
|
||||
"Type `{}` does not exist at path `{}`\n{}",
|
||||
ident.to_string(),
|
||||
pretty_path(path),
|
||||
alternatives
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
#[test]
|
||||
fn parse_types() {
|
||||
use crate::commands::codegen::type_map_parser;
|
||||
|
||||
assert_eq!(type_map_parser("Foo"), None);
|
||||
assert_eq!(type_map_parser("Foo=Bar"), Some(("Foo", "Bar", false)));
|
||||
assert_eq!(
|
||||
type_map_parser("Foo=Bar,recursive"),
|
||||
Some(("Foo", "Bar", true))
|
||||
);
|
||||
assert_eq!(type_map_parser("Foo=Bar,a"), None);
|
||||
assert_eq!(type_map_parser("Foo=Bar,a,b,c,recursive"), None);
|
||||
}
|
||||
|
||||
async fn run(args_str: &str) -> color_eyre::Result<String> {
|
||||
let mut args = vec![
|
||||
"codegen",
|
||||
"--file=../artifacts/polkadot_metadata_small.scale",
|
||||
];
|
||||
args.extend(args_str.split(' ').filter(|e| !e.is_empty()));
|
||||
let opts: super::Opts = clap::Parser::try_parse_from(args)?;
|
||||
let mut output: Vec<u8> = Vec::new();
|
||||
let r = super::run(opts, &mut output)
|
||||
.await
|
||||
.map(|_| String::from_utf8(output).unwrap())?;
|
||||
Ok(r)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn invalid_type_paths() {
|
||||
let valid_type = "sp_runtime::multiaddress::MultiAddress";
|
||||
let invalid_type = "my_module::MultiAddress";
|
||||
|
||||
let valid_cases = [
|
||||
format!("--derive-for-type {valid_type}=serde::Serialize"),
|
||||
format!("--attributes-for-type {valid_type}=#[allow(clippy::all)]"),
|
||||
format!("--substitute-type {valid_type}=::my_crate::MultiAddress"),
|
||||
];
|
||||
for case in valid_cases.iter() {
|
||||
let output = run(case).await;
|
||||
assert!(output.is_ok());
|
||||
}
|
||||
|
||||
let invalid_cases = [
|
||||
format!("--derive-for-type {invalid_type}=serde::Serialize"),
|
||||
format!("--attributes-for-type {invalid_type}=#[allow(clippy::all)]"),
|
||||
format!("--substitute-type {invalid_type}=my_module::MultiAddress"),
|
||||
];
|
||||
for case in invalid_cases.iter() {
|
||||
let output = run(case).await;
|
||||
// assert that we make suggestions pointing the user to the valid type
|
||||
assert!(output.unwrap_err().to_string().contains(valid_type));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
use clap::Parser as ClapParser;
|
||||
use codec::Decode;
|
||||
use color_eyre::eyre::WrapErr;
|
||||
use jsonrpsee::client_transport::ws::Url;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use pezkuwi_subxt_metadata::Metadata;
|
||||
use pezkuwi_subxt_utils_fetchmetadata::MetadataVersion;
|
||||
|
||||
use crate::utils::validate_url_security;
|
||||
|
||||
/// Verify metadata compatibility between substrate nodes.
|
||||
#[derive(Debug, ClapParser)]
|
||||
pub struct Opts {
|
||||
/// Urls of the substrate nodes to verify for metadata compatibility.
|
||||
#[clap(name = "nodes", long, use_value_delimiter = true, value_parser)]
|
||||
nodes: Vec<Url>,
|
||||
/// Check the compatibility of metadata for a particular pallet.
|
||||
///
|
||||
/// ### Note
|
||||
/// The validation will omit the full metadata check and focus instead on the pallet.
|
||||
#[clap(long, value_parser)]
|
||||
pallet: Option<String>,
|
||||
/// Specify the metadata version.
|
||||
///
|
||||
/// - unstable:
|
||||
///
|
||||
/// Use the latest unstable metadata of the node.
|
||||
///
|
||||
/// - number
|
||||
///
|
||||
/// Use this specific metadata version.
|
||||
///
|
||||
/// Defaults to latest.
|
||||
#[clap(long = "version", default_value = "latest")]
|
||||
version: MetadataVersion,
|
||||
/// Allow insecure URLs e.g. URLs starting with ws:// or http:// without SSL encryption
|
||||
#[clap(long, short)]
|
||||
allow_insecure: bool,
|
||||
}
|
||||
|
||||
pub async fn run(opts: Opts, output: &mut impl std::io::Write) -> color_eyre::Result<()> {
|
||||
for url in opts.nodes.iter() {
|
||||
validate_url_security(Some(url), opts.allow_insecure)?;
|
||||
}
|
||||
|
||||
match opts.pallet {
|
||||
Some(pallet) => {
|
||||
handle_pallet_metadata(opts.nodes.as_slice(), pallet.as_str(), opts.version, output)
|
||||
.await
|
||||
}
|
||||
None => handle_full_metadata(opts.nodes.as_slice(), opts.version, output).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_pallet_metadata(
|
||||
nodes: &[Url],
|
||||
name: &str,
|
||||
version: MetadataVersion,
|
||||
output: &mut impl std::io::Write,
|
||||
) -> color_eyre::Result<()> {
|
||||
#[derive(Serialize, Deserialize, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct CompatibilityPallet {
|
||||
pallet_present: HashMap<String, Vec<String>>,
|
||||
pallet_not_found: Vec<String>,
|
||||
}
|
||||
|
||||
let mut compatibility: CompatibilityPallet = Default::default();
|
||||
for node in nodes.iter() {
|
||||
let metadata = fetch_runtime_metadata(node.clone(), version).await?;
|
||||
|
||||
match metadata.pallet_by_name(name) {
|
||||
Some(pallet_metadata) => {
|
||||
let hash = pallet_metadata.hash();
|
||||
let hex_hash = hex::encode(hash);
|
||||
writeln!(
|
||||
output,
|
||||
"Node {node:?} has pallet metadata hash {hex_hash:?}"
|
||||
)?;
|
||||
|
||||
compatibility
|
||||
.pallet_present
|
||||
.entry(hex_hash)
|
||||
.or_default()
|
||||
.push(node.to_string());
|
||||
}
|
||||
None => {
|
||||
compatibility.pallet_not_found.push(node.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
writeln!(
|
||||
output,
|
||||
"\nCompatible nodes by pallet\n{}",
|
||||
serde_json::to_string_pretty(&compatibility)
|
||||
.context("Failed to parse compatibility map")?
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_full_metadata(
|
||||
nodes: &[Url],
|
||||
version: MetadataVersion,
|
||||
output: &mut impl std::io::Write,
|
||||
) -> color_eyre::Result<()> {
|
||||
let mut compatibility_map: HashMap<String, Vec<String>> = HashMap::new();
|
||||
for node in nodes.iter() {
|
||||
let metadata = fetch_runtime_metadata(node.clone(), version).await?;
|
||||
let hash = metadata.hasher().hash();
|
||||
let hex_hash = hex::encode(hash);
|
||||
writeln!(output, "Node {node:?} has metadata hash {hex_hash:?}",)?;
|
||||
|
||||
compatibility_map
|
||||
.entry(hex_hash)
|
||||
.or_default()
|
||||
.push(node.to_string());
|
||||
}
|
||||
|
||||
writeln!(
|
||||
output,
|
||||
"\nCompatible nodes\n{}",
|
||||
serde_json::to_string_pretty(&compatibility_map)
|
||||
.context("Failed to parse compatibility map")?
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn fetch_runtime_metadata(
|
||||
url: Url,
|
||||
version: MetadataVersion,
|
||||
) -> color_eyre::Result<Metadata> {
|
||||
let bytes = pezkuwi_subxt_utils_fetchmetadata::from_url(url, version, None).await?;
|
||||
let metadata = Metadata::decode(&mut &bytes[..])?;
|
||||
Ok(metadata)
|
||||
}
|
||||
+450
@@ -0,0 +1,450 @@
|
||||
use clap::Args;
|
||||
use codec::Decode;
|
||||
|
||||
use frame_metadata::RuntimeMetadataPrefixed;
|
||||
use std::collections::HashMap;
|
||||
use std::hash::Hash;
|
||||
|
||||
use crate::utils::{FileOrUrl, validate_url_security};
|
||||
use color_eyre::owo_colors::OwoColorize;
|
||||
|
||||
use scale_info::Variant;
|
||||
use scale_info::form::PortableForm;
|
||||
|
||||
use pezkuwi_subxt_metadata::{
|
||||
ConstantMetadata, Metadata, PalletMetadata, RuntimeApiMetadata, StorageEntryMetadata,
|
||||
};
|
||||
|
||||
/// Explore the differences between two nodes
|
||||
///
|
||||
/// # Example
|
||||
/// ```text
|
||||
/// subxt diff ./artifacts/polkadot_metadata_small.scale ./artifacts/polkadot_metadata_tiny.scale
|
||||
/// subxt diff ./artifacts/polkadot_metadata_small.scale wss://rpc.polkadot.io:443
|
||||
/// ```
|
||||
#[derive(Debug, Args)]
|
||||
#[command(author, version, about, long_about = None)]
|
||||
pub struct Opts {
|
||||
/// metadata file or node URL
|
||||
metadata_or_url_1: FileOrUrl,
|
||||
/// metadata file or node URL
|
||||
metadata_or_url_2: FileOrUrl,
|
||||
/// Allow insecure URLs e.g. URLs starting with ws:// or http:// without SSL encryption
|
||||
#[clap(long, short)]
|
||||
allow_insecure: bool,
|
||||
}
|
||||
|
||||
pub async fn run(opts: Opts, output: &mut impl std::io::Write) -> color_eyre::Result<()> {
|
||||
validate_url_security(opts.metadata_or_url_1.url.as_ref(), opts.allow_insecure)?;
|
||||
validate_url_security(opts.metadata_or_url_2.url.as_ref(), opts.allow_insecure)?;
|
||||
|
||||
let (entry_1_metadata, entry_2_metadata) = get_metadata(&opts).await?;
|
||||
|
||||
let metadata_diff = MetadataDiff::construct(&entry_1_metadata, &entry_2_metadata);
|
||||
|
||||
if metadata_diff.is_empty() {
|
||||
writeln!(output, "No difference in metadata found.")?;
|
||||
return Ok(());
|
||||
}
|
||||
if !metadata_diff.pallets.is_empty() {
|
||||
writeln!(output, "Pallets:")?;
|
||||
for diff in metadata_diff.pallets {
|
||||
match diff {
|
||||
Diff::Added(new) => {
|
||||
writeln!(output, "{}", format!(" + {}", new.name()).green())?
|
||||
}
|
||||
Diff::Removed(old) => {
|
||||
writeln!(output, "{}", format!(" - {}", old.name()).red())?
|
||||
}
|
||||
Diff::Changed { from, to } => {
|
||||
writeln!(output, "{}", format!(" ~ {}", from.name()).yellow())?;
|
||||
|
||||
let pallet_diff = PalletDiff::construct(&from, &to);
|
||||
if !pallet_diff.calls.is_empty() {
|
||||
writeln!(output, " Calls:")?;
|
||||
for diff in pallet_diff.calls {
|
||||
match diff {
|
||||
Diff::Added(new) => writeln!(
|
||||
output,
|
||||
"{}",
|
||||
format!(" + {}", &new.name).green()
|
||||
)?,
|
||||
Diff::Removed(old) => writeln!(
|
||||
output,
|
||||
"{}",
|
||||
format!(" - {}", &old.name).red()
|
||||
)?,
|
||||
Diff::Changed { from, to: _ } => {
|
||||
writeln!(
|
||||
output,
|
||||
"{}",
|
||||
format!(" ~ {}", &from.name).yellow()
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !pallet_diff.constants.is_empty() {
|
||||
writeln!(output, " Constants:")?;
|
||||
for diff in pallet_diff.constants {
|
||||
match diff {
|
||||
Diff::Added(new) => writeln!(
|
||||
output,
|
||||
"{}",
|
||||
format!(" + {}", new.name()).green()
|
||||
)?,
|
||||
Diff::Removed(old) => writeln!(
|
||||
output,
|
||||
"{}",
|
||||
format!(" - {}", old.name()).red()
|
||||
)?,
|
||||
Diff::Changed { from, to: _ } => writeln!(
|
||||
output,
|
||||
"{}",
|
||||
format!(" ~ {}", from.name()).yellow()
|
||||
)?,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !pallet_diff.storage_entries.is_empty() {
|
||||
writeln!(output, " Storage Entries:")?;
|
||||
for diff in pallet_diff.storage_entries {
|
||||
match diff {
|
||||
Diff::Added(new) => writeln!(
|
||||
output,
|
||||
"{}",
|
||||
format!(" + {}", new.name()).green()
|
||||
)?,
|
||||
Diff::Removed(old) => writeln!(
|
||||
output,
|
||||
"{}",
|
||||
format!(" - {}", old.name()).red()
|
||||
)?,
|
||||
Diff::Changed { from, to } => {
|
||||
let storage_diff = StorageEntryDiff::construct(
|
||||
from,
|
||||
to,
|
||||
&entry_1_metadata,
|
||||
&entry_2_metadata,
|
||||
);
|
||||
|
||||
writeln!(
|
||||
output,
|
||||
"{}",
|
||||
format!(
|
||||
" ~ {} (Changed: {})",
|
||||
from.name(),
|
||||
storage_diff.to_strings().join(", ")
|
||||
)
|
||||
.yellow()
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !metadata_diff.runtime_apis.is_empty() {
|
||||
writeln!(output, "Runtime APIs:")?;
|
||||
for diff in metadata_diff.runtime_apis {
|
||||
match diff {
|
||||
Diff::Added(new) => {
|
||||
writeln!(output, "{}", format!(" + {}", new.name()).green())?
|
||||
}
|
||||
Diff::Removed(old) => {
|
||||
writeln!(output, "{}", format!(" - {}", old.name()).red())?
|
||||
}
|
||||
Diff::Changed { from, to: _ } => {
|
||||
writeln!(output, "{}", format!(" ~ {}", from.name()).yellow())?
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct MetadataDiff<'a> {
|
||||
pallets: Vec<Diff<PalletMetadata<'a>>>,
|
||||
runtime_apis: Vec<Diff<RuntimeApiMetadata<'a>>>,
|
||||
}
|
||||
|
||||
impl<'a> MetadataDiff<'a> {
|
||||
fn construct(metadata_1: &'a Metadata, metadata_2: &'a Metadata) -> MetadataDiff<'a> {
|
||||
let pallets = pallet_differences(metadata_1, metadata_2);
|
||||
let runtime_apis = runtime_api_differences(metadata_1, metadata_2);
|
||||
MetadataDiff {
|
||||
pallets,
|
||||
runtime_apis,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_empty(&self) -> bool {
|
||||
self.pallets.is_empty() && self.runtime_apis.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct PalletDiff<'a> {
|
||||
calls: Vec<Diff<&'a Variant<PortableForm>>>,
|
||||
constants: Vec<Diff<&'a ConstantMetadata>>,
|
||||
storage_entries: Vec<Diff<&'a StorageEntryMetadata>>,
|
||||
}
|
||||
|
||||
impl<'a> PalletDiff<'a> {
|
||||
fn construct(
|
||||
pallet_metadata_1: &'a PalletMetadata<'a>,
|
||||
pallet_metadata_2: &'a PalletMetadata<'a>,
|
||||
) -> PalletDiff<'a> {
|
||||
let calls = calls_differences(pallet_metadata_1, pallet_metadata_2);
|
||||
let constants = constants_differences(pallet_metadata_1, pallet_metadata_2);
|
||||
let storage_entries = storage_differences(pallet_metadata_1, pallet_metadata_2);
|
||||
PalletDiff {
|
||||
calls,
|
||||
constants,
|
||||
storage_entries,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct StorageEntryDiff {
|
||||
key_different: bool,
|
||||
value_different: bool,
|
||||
default_different: bool,
|
||||
}
|
||||
|
||||
impl StorageEntryDiff {
|
||||
fn construct(
|
||||
storage_entry_1: &StorageEntryMetadata,
|
||||
storage_entry_2: &StorageEntryMetadata,
|
||||
metadata_1: &Metadata,
|
||||
metadata_2: &Metadata,
|
||||
) -> Self {
|
||||
let value_1_ty_id = storage_entry_1.value_ty();
|
||||
let value_1_hash = metadata_1
|
||||
.type_hash(value_1_ty_id)
|
||||
.expect("type is in metadata; qed");
|
||||
let value_2_ty_id = storage_entry_2.value_ty();
|
||||
let value_2_hash = metadata_2
|
||||
.type_hash(value_2_ty_id)
|
||||
.expect("type is in metadata; qed");
|
||||
let value_different = value_1_hash != value_2_hash;
|
||||
|
||||
let key_parts_same = storage_entry_1.keys().len() == storage_entry_2.keys().len()
|
||||
&& storage_entry_1
|
||||
.keys()
|
||||
.zip(storage_entry_2.keys())
|
||||
.all(|(a, b)| {
|
||||
let a_hash = metadata_1.type_hash(a.key_id).expect("type is in metadata");
|
||||
let b_hash = metadata_2.type_hash(b.key_id).expect("type is in metadata");
|
||||
a.hasher == b.hasher && a_hash == b_hash
|
||||
});
|
||||
|
||||
let key_different = !key_parts_same;
|
||||
|
||||
StorageEntryDiff {
|
||||
key_different,
|
||||
value_different,
|
||||
default_different: storage_entry_1.default_value() != storage_entry_2.default_value(),
|
||||
}
|
||||
}
|
||||
|
||||
fn to_strings(&self) -> Vec<&str> {
|
||||
let mut strings = Vec::<&str>::new();
|
||||
if self.key_different {
|
||||
strings.push("key type");
|
||||
}
|
||||
if self.value_different {
|
||||
strings.push("value type");
|
||||
}
|
||||
if self.default_different {
|
||||
strings.push("default value");
|
||||
}
|
||||
strings
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_metadata(opts: &Opts) -> color_eyre::Result<(Metadata, Metadata)> {
|
||||
let bytes = opts.metadata_or_url_1.fetch().await?;
|
||||
let entry_1_metadata: Metadata =
|
||||
RuntimeMetadataPrefixed::decode(&mut &bytes[..])?.try_into()?;
|
||||
|
||||
let bytes = opts.metadata_or_url_2.fetch().await?;
|
||||
let entry_2_metadata: Metadata =
|
||||
RuntimeMetadataPrefixed::decode(&mut &bytes[..])?.try_into()?;
|
||||
|
||||
Ok((entry_1_metadata, entry_2_metadata))
|
||||
}
|
||||
|
||||
fn storage_differences<'a>(
|
||||
pallet_metadata_1: &'a PalletMetadata<'a>,
|
||||
pallet_metadata_2: &'a PalletMetadata<'a>,
|
||||
) -> Vec<Diff<&'a StorageEntryMetadata>> {
|
||||
diff(
|
||||
pallet_metadata_1
|
||||
.storage()
|
||||
.map(|s| s.entries())
|
||||
.unwrap_or_default(),
|
||||
pallet_metadata_2
|
||||
.storage()
|
||||
.map(|s| s.entries())
|
||||
.unwrap_or_default(),
|
||||
|e| {
|
||||
pallet_metadata_1
|
||||
.storage_hash(e.name())
|
||||
.expect("storage entry is in metadata; qed")
|
||||
},
|
||||
|e| {
|
||||
pallet_metadata_2
|
||||
.storage_hash(e.name())
|
||||
.expect("storage entry is in metadata; qed")
|
||||
},
|
||||
|e| e.name(),
|
||||
)
|
||||
}
|
||||
|
||||
fn calls_differences<'a>(
|
||||
pallet_metadata_1: &'a PalletMetadata<'a>,
|
||||
pallet_metadata_2: &'a PalletMetadata<'a>,
|
||||
) -> Vec<Diff<&'a Variant<PortableForm>>> {
|
||||
diff(
|
||||
pallet_metadata_1.call_variants().unwrap_or_default(),
|
||||
pallet_metadata_2.call_variants().unwrap_or_default(),
|
||||
|e| {
|
||||
pallet_metadata_1
|
||||
.call_hash(&e.name)
|
||||
.expect("call is in metadata; qed")
|
||||
},
|
||||
|e| {
|
||||
pallet_metadata_2
|
||||
.call_hash(&e.name)
|
||||
.expect("call is in metadata; qed")
|
||||
},
|
||||
|e| &e.name,
|
||||
)
|
||||
}
|
||||
|
||||
fn constants_differences<'a>(
|
||||
pallet_metadata_1: &'a PalletMetadata<'a>,
|
||||
pallet_metadata_2: &'a PalletMetadata<'a>,
|
||||
) -> Vec<Diff<&'a ConstantMetadata>> {
|
||||
diff(
|
||||
pallet_metadata_1.constants(),
|
||||
pallet_metadata_2.constants(),
|
||||
|e| {
|
||||
pallet_metadata_1
|
||||
.constant_hash(e.name())
|
||||
.expect("constant is in metadata; qed")
|
||||
},
|
||||
|e| {
|
||||
pallet_metadata_2
|
||||
.constant_hash(e.name())
|
||||
.expect("constant is in metadata; qed")
|
||||
},
|
||||
|e| e.name(),
|
||||
)
|
||||
}
|
||||
|
||||
fn runtime_api_differences<'a>(
|
||||
metadata_1: &'a Metadata,
|
||||
metadata_2: &'a Metadata,
|
||||
) -> Vec<Diff<RuntimeApiMetadata<'a>>> {
|
||||
diff(
|
||||
metadata_1.runtime_api_traits(),
|
||||
metadata_2.runtime_api_traits(),
|
||||
RuntimeApiMetadata::hash,
|
||||
RuntimeApiMetadata::hash,
|
||||
RuntimeApiMetadata::name,
|
||||
)
|
||||
}
|
||||
|
||||
fn pallet_differences<'a>(
|
||||
metadata_1: &'a Metadata,
|
||||
metadata_2: &'a Metadata,
|
||||
) -> Vec<Diff<PalletMetadata<'a>>> {
|
||||
diff(
|
||||
metadata_1.pallets(),
|
||||
metadata_2.pallets(),
|
||||
PalletMetadata::hash,
|
||||
PalletMetadata::hash,
|
||||
PalletMetadata::name,
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
enum Diff<T> {
|
||||
Added(T),
|
||||
Changed { from: T, to: T },
|
||||
Removed(T),
|
||||
}
|
||||
|
||||
fn diff<T, C: PartialEq, I: Hash + PartialEq + Eq + Ord>(
|
||||
items_a: impl IntoIterator<Item = T>,
|
||||
items_b: impl IntoIterator<Item = T>,
|
||||
hash_fn_a: impl Fn(&T) -> C,
|
||||
hash_fn_b: impl Fn(&T) -> C,
|
||||
key_fn: impl Fn(&T) -> I,
|
||||
) -> Vec<Diff<T>> {
|
||||
let mut entries: HashMap<I, (Option<T>, Option<T>)> = HashMap::new();
|
||||
|
||||
for t1 in items_a {
|
||||
let key = key_fn(&t1);
|
||||
let (e1, _) = entries.entry(key).or_default();
|
||||
*e1 = Some(t1);
|
||||
}
|
||||
|
||||
for t2 in items_b {
|
||||
let key = key_fn(&t2);
|
||||
let (e1, e2) = entries.entry(key).or_default();
|
||||
// skip all entries with the same hash:
|
||||
if let Some(e1_inner) = e1 {
|
||||
let e1_hash = hash_fn_a(e1_inner);
|
||||
let e2_hash = hash_fn_b(&t2);
|
||||
if e1_hash == e2_hash {
|
||||
entries.remove(&key_fn(&t2));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
*e2 = Some(t2);
|
||||
}
|
||||
|
||||
// sort the values by key before returning
|
||||
let mut diff_vec_with_keys: Vec<_> = entries.into_iter().collect();
|
||||
diff_vec_with_keys.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
diff_vec_with_keys
|
||||
.into_iter()
|
||||
.map(|(_, tuple)| match tuple {
|
||||
(None, None) => panic!("At least one value is inserted when the key exists; qed"),
|
||||
(Some(old), None) => Diff::Removed(old),
|
||||
(None, Some(new)) => Diff::Added(new),
|
||||
(Some(old), Some(new)) => Diff::Changed { from: old, to: new },
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::commands::diff::{Diff, diff};
|
||||
|
||||
#[test]
|
||||
fn test_diff_fn() {
|
||||
let old_pallets = [("Babe", 7), ("Claims", 9), ("Balances", 23)];
|
||||
let new_pallets = [("Claims", 9), ("Balances", 22), ("System", 3), ("NFTs", 5)];
|
||||
let hash_fn = |e: &(&str, i32)| e.0.len() as i32 * e.1;
|
||||
let differences = diff(old_pallets, new_pallets, hash_fn, hash_fn, |e| e.0);
|
||||
let expected_differences = vec![
|
||||
Diff::Removed(("Babe", 7)),
|
||||
Diff::Changed {
|
||||
from: ("Balances", 23),
|
||||
to: ("Balances", 22),
|
||||
},
|
||||
Diff::Added(("NFTs", 5)),
|
||||
Diff::Added(("System", 3)),
|
||||
];
|
||||
assert_eq!(differences, expected_differences);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,469 @@
|
||||
use crate::utils::FileOrUrl;
|
||||
use crate::utils::validate_url_security;
|
||||
use clap::{Parser, Subcommand, command};
|
||||
use codec::Decode;
|
||||
use color_eyre::eyre::eyre;
|
||||
use color_eyre::owo_colors::OwoColorize;
|
||||
use indoc::writedoc;
|
||||
use std::fmt::Write;
|
||||
use std::write;
|
||||
|
||||
use subxt::Metadata;
|
||||
|
||||
use self::pallets::PalletSubcommand;
|
||||
|
||||
mod pallets;
|
||||
mod runtime_apis;
|
||||
|
||||
/// Explore pallets, calls, call parameters, storage entries and constants. Also allows for creating (unsigned) extrinsics.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// Show the pallets and runtime apis that are available:
|
||||
///
|
||||
/// ```text
|
||||
/// subxt explore --file=polkadot_metadata.scale
|
||||
/// ```
|
||||
///
|
||||
/// ## Pallets
|
||||
///
|
||||
/// each pallet has `calls`, `constants`, `storage` and `events` that can be explored.
|
||||
///
|
||||
/// ### Calls
|
||||
///
|
||||
/// Show the calls in a pallet:
|
||||
///
|
||||
/// ```text
|
||||
/// subxt explore pallet Balances calls
|
||||
/// ```
|
||||
///
|
||||
/// Show the call parameters a call expects:
|
||||
///
|
||||
/// ```text
|
||||
/// subxt explore pallet Balances calls transfer
|
||||
/// ```
|
||||
///
|
||||
/// Create an unsigned extrinsic from a scale value, validate it and output its hex representation
|
||||
///
|
||||
/// ```text
|
||||
/// subxt explore pallet Grandpa calls note_stalled { "delay": 5, "best_finalized_block_number": 5 }
|
||||
/// # Encoded call data:
|
||||
/// # 0x2c0411020500000005000000
|
||||
/// subxt explore pallet Balances calls transfer "{ \"dest\": v\"Raw\"((255, 255, 255)), \"value\": 0 }"
|
||||
/// # Encoded call data:
|
||||
/// # 0x24040607020cffffff00
|
||||
/// ```
|
||||
///
|
||||
/// ### Constants
|
||||
///
|
||||
/// Show the constants in a pallet:
|
||||
///
|
||||
/// ```text
|
||||
/// subxt explore pallet Balances constants
|
||||
/// ```
|
||||
///
|
||||
/// ### Storage
|
||||
///
|
||||
/// Show the storage entries in a pallet
|
||||
///
|
||||
/// ```text
|
||||
/// subxt explore pallet Alliance storage
|
||||
/// ```
|
||||
///
|
||||
/// Show the types and value of a specific storage entry
|
||||
///
|
||||
/// ```text
|
||||
/// subxt explore pallet Alliance storage Announcements [KEY_SCALE_VALUE]
|
||||
/// ```
|
||||
///
|
||||
/// ### Events
|
||||
///
|
||||
/// ```text
|
||||
/// subxt explore pallet Balances events
|
||||
/// ```
|
||||
///
|
||||
/// Show the type of a specific event
|
||||
///
|
||||
/// ```text
|
||||
/// subxt explore pallet Balances events frozen
|
||||
/// ```
|
||||
///
|
||||
/// ## Runtime APIs
|
||||
/// Show the input and output types of a runtime api method.
|
||||
/// In this example "core" is the name of the runtime api and "version" is a method on it:
|
||||
///
|
||||
/// ```text
|
||||
/// subxt explore api core version
|
||||
/// ```
|
||||
///
|
||||
/// Execute a runtime API call with the `--execute` (`-e`) flag, to see the return value.
|
||||
/// For example here we get the "version", via the "core" runtime API from the connected node:
|
||||
///
|
||||
/// ```text
|
||||
/// subxt explore api core version --execute
|
||||
/// ```
|
||||
///
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct Opts {
|
||||
#[command(flatten)]
|
||||
file_or_url: FileOrUrl,
|
||||
#[command(subcommand)]
|
||||
subcommand: Option<PalletOrRuntimeApi>,
|
||||
/// Allow insecure URLs e.g. URLs starting with ws:// or http:// without SSL encryption
|
||||
#[clap(long, short)]
|
||||
allow_insecure: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum PalletOrRuntimeApi {
|
||||
Pallet(PalletOpts),
|
||||
Api(RuntimeApiOpts),
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct PalletOpts {
|
||||
pub name: Option<String>,
|
||||
#[command(subcommand)]
|
||||
pub subcommand: Option<PalletSubcommand>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct RuntimeApiOpts {
|
||||
pub name: Option<String>,
|
||||
#[clap(required = false)]
|
||||
pub method: Option<String>,
|
||||
#[clap(long, short, action)]
|
||||
pub execute: bool,
|
||||
#[clap(required = false)]
|
||||
trailing_args: Vec<String>,
|
||||
}
|
||||
|
||||
pub async fn run(opts: Opts, output: &mut impl std::io::Write) -> color_eyre::Result<()> {
|
||||
validate_url_security(opts.file_or_url.url.as_ref(), opts.allow_insecure)?;
|
||||
|
||||
// get the metadata
|
||||
let file_or_url = opts.file_or_url;
|
||||
let bytes = file_or_url.fetch().await?;
|
||||
let metadata = Metadata::decode(&mut &bytes[..])?;
|
||||
|
||||
let pallet_placeholder = "<PALLET>".blue();
|
||||
let runtime_api_placeholder = "<RUNTIME_API>".blue();
|
||||
|
||||
// if no pallet/runtime_api specified, show user the pallets/runtime_apis to choose from:
|
||||
let Some(pallet_or_runtime_api) = opts.subcommand else {
|
||||
let pallets = pallets_as_string(&metadata);
|
||||
let runtime_apis = runtime_apis_as_string(&metadata);
|
||||
writedoc! {output, "
|
||||
Usage:
|
||||
subxt explore pallet {pallet_placeholder}
|
||||
explore a specific pallet
|
||||
subxt explore api {runtime_api_placeholder}
|
||||
explore a specific runtime api
|
||||
|
||||
{pallets}
|
||||
|
||||
{runtime_apis}
|
||||
"}?;
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
match pallet_or_runtime_api {
|
||||
PalletOrRuntimeApi::Pallet(opts) => {
|
||||
let Some(name) = opts.name else {
|
||||
let pallets = pallets_as_string(&metadata);
|
||||
writedoc! {output, "
|
||||
Usage:
|
||||
subxt explore pallet {pallet_placeholder}
|
||||
explore a specific pallet
|
||||
|
||||
{pallets}
|
||||
"}?;
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
if let Some(pallet) = metadata
|
||||
.pallets()
|
||||
.find(|e| e.name().eq_ignore_ascii_case(&name))
|
||||
{
|
||||
pallets::run(opts.subcommand, pallet, &metadata, file_or_url, output).await
|
||||
} else {
|
||||
Err(eyre!(
|
||||
"pallet \"{name}\" not found in metadata!\n{}",
|
||||
pallets_as_string(&metadata),
|
||||
))
|
||||
}
|
||||
}
|
||||
PalletOrRuntimeApi::Api(opts) => {
|
||||
let Some(name) = opts.name else {
|
||||
let runtime_apis = runtime_apis_as_string(&metadata);
|
||||
writedoc! {output, "
|
||||
Usage:
|
||||
subxt explore api {runtime_api_placeholder}
|
||||
explore a specific runtime api
|
||||
|
||||
{runtime_apis}
|
||||
"}?;
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
if let Some(runtime_api) = metadata
|
||||
.runtime_api_traits()
|
||||
.find(|e| e.name().eq_ignore_ascii_case(&name))
|
||||
{
|
||||
runtime_apis::run(
|
||||
opts.method,
|
||||
opts.execute,
|
||||
opts.trailing_args,
|
||||
runtime_api,
|
||||
&metadata,
|
||||
file_or_url,
|
||||
output,
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
Err(eyre!(
|
||||
"runtime api \"{name}\" not found in metadata!\n{}",
|
||||
runtime_apis_as_string(&metadata),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn pallets_as_string(metadata: &Metadata) -> String {
|
||||
let pallet_placeholder = "<PALLET>".blue();
|
||||
if metadata.pallets().len() == 0 {
|
||||
format!("There are no {pallet_placeholder}'s available.")
|
||||
} else {
|
||||
let mut output = format!("Available {pallet_placeholder}'s are:");
|
||||
let mut strings: Vec<_> = metadata.pallets().map(|p| p.name()).collect();
|
||||
strings.sort();
|
||||
for pallet in strings {
|
||||
write!(output, "\n {pallet}").unwrap();
|
||||
}
|
||||
output
|
||||
}
|
||||
}
|
||||
|
||||
pub fn runtime_apis_as_string(metadata: &Metadata) -> String {
|
||||
let runtime_api_placeholder = "<RUNTIME_API>".blue();
|
||||
if metadata.runtime_api_traits().len() == 0 {
|
||||
format!("There are no {runtime_api_placeholder}'s available.")
|
||||
} else {
|
||||
let mut output = format!("Available {runtime_api_placeholder}'s are:");
|
||||
let mut strings: Vec<_> = metadata.runtime_api_traits().map(|p| p.name()).collect();
|
||||
strings.sort();
|
||||
for api in strings {
|
||||
write!(output, "\n {api}").unwrap();
|
||||
}
|
||||
output
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
|
||||
use indoc::formatdoc;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::Opts;
|
||||
|
||||
async fn run(cli_command: &str) -> color_eyre::Result<String> {
|
||||
let mut args = vec!["explore"];
|
||||
let mut split: Vec<&str> = cli_command.split(' ').filter(|e| !e.is_empty()).collect();
|
||||
args.append(&mut split);
|
||||
let opts: Opts = clap::Parser::try_parse_from(args)?;
|
||||
let mut output: Vec<u8> = Vec::new();
|
||||
let r = super::run(opts, &mut output)
|
||||
.await
|
||||
.map(|_| String::from_utf8(output).unwrap())?;
|
||||
Ok(r)
|
||||
}
|
||||
|
||||
trait StripAnsi: ToString {
|
||||
fn strip_ansi(&self) -> String {
|
||||
let bytes = strip_ansi_escapes::strip(self.to_string().as_bytes());
|
||||
String::from_utf8(bytes).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ToString> StripAnsi for T {}
|
||||
|
||||
macro_rules! assert_eq_start {
|
||||
($a:expr, $b:expr) => {
|
||||
assert_eq!(&$a[0..$b.len()], &$b[..]);
|
||||
};
|
||||
}
|
||||
|
||||
async fn run_against_file(cli_command: &str) -> color_eyre::Result<String> {
|
||||
run(&format!(
|
||||
"--file=../artifacts/polkadot_metadata_small.scale {cli_command}"
|
||||
))
|
||||
.await
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_commands() {
|
||||
// shows pallets and runtime apis:
|
||||
let output = run_against_file("").await.unwrap().strip_ansi();
|
||||
let expected_output = formatdoc! {
|
||||
"Usage:
|
||||
subxt explore pallet <PALLET>
|
||||
explore a specific pallet
|
||||
subxt explore api <RUNTIME_API>
|
||||
explore a specific runtime api
|
||||
|
||||
Available <PALLET>'s are:
|
||||
Balances
|
||||
Multisig
|
||||
ParaInherent
|
||||
System
|
||||
Timestamp
|
||||
|
||||
Available <RUNTIME_API>'s are:
|
||||
AccountNonceApi
|
||||
AuthorityDiscoveryApi
|
||||
BabeApi
|
||||
BeefyApi
|
||||
BeefyMmrApi
|
||||
BlockBuilder
|
||||
Core
|
||||
DryRunApi
|
||||
GenesisBuilder
|
||||
GrandpaApi
|
||||
LocationToAccountApi
|
||||
Metadata
|
||||
MmrApi
|
||||
OffchainWorkerApi
|
||||
ParachainHost
|
||||
SessionKeys
|
||||
TaggedTransactionQueue
|
||||
TransactionPaymentApi
|
||||
TrustedQueryApi
|
||||
XcmPaymentApi
|
||||
"};
|
||||
assert_eq!(output, expected_output);
|
||||
// if incorrect pallet, error:
|
||||
let output = run_against_file("abc123").await;
|
||||
assert!(output.is_err());
|
||||
// if correct pallet, show options (calls, constants, storage)
|
||||
let output = run_against_file("pallet Balances")
|
||||
.await
|
||||
.unwrap()
|
||||
.strip_ansi();
|
||||
let expected_output = formatdoc! {"
|
||||
Usage:
|
||||
subxt explore pallet Balances calls
|
||||
explore the calls that can be made into a pallet
|
||||
subxt explore pallet Balances constants
|
||||
explore the constants of a pallet
|
||||
subxt explore pallet Balances storage
|
||||
explore the storage values of a pallet
|
||||
subxt explore pallet Balances events
|
||||
explore the events of a pallet
|
||||
"};
|
||||
assert_eq!(output, expected_output);
|
||||
// check that exploring calls, storage entries and constants is possible:
|
||||
let output = run_against_file("pallet Balances calls")
|
||||
.await
|
||||
.unwrap()
|
||||
.strip_ansi();
|
||||
let start = formatdoc! {"
|
||||
Usage:
|
||||
subxt explore pallet Balances calls <CALL>
|
||||
explore a specific call of this pallet
|
||||
|
||||
Available <CALL>'s in the \"Balances\" pallet:"};
|
||||
assert_eq_start!(output, start);
|
||||
let output = run_against_file("pallet Balances storage")
|
||||
.await
|
||||
.unwrap()
|
||||
.strip_ansi();
|
||||
let start = formatdoc! {"
|
||||
Usage:
|
||||
subxt explore pallet Balances storage <STORAGE_ENTRY>
|
||||
explore a specific storage entry of this pallet
|
||||
|
||||
Available <STORAGE_ENTRY>'s in the \"Balances\" pallet:
|
||||
"};
|
||||
assert_eq_start!(output, start);
|
||||
let output = run_against_file("pallet Balances constants")
|
||||
.await
|
||||
.unwrap()
|
||||
.strip_ansi();
|
||||
let start = formatdoc! {"
|
||||
Usage:
|
||||
subxt explore pallet Balances constants <CONSTANT>
|
||||
explore a specific constant of this pallet
|
||||
|
||||
Available <CONSTANT>'s in the \"Balances\" pallet:
|
||||
"};
|
||||
assert_eq_start!(output, start);
|
||||
let output = run_against_file("pallet Balances events")
|
||||
.await
|
||||
.unwrap()
|
||||
.strip_ansi();
|
||||
let start = formatdoc! {"
|
||||
Usage:
|
||||
subxt explore pallet Balances events <EVENT>
|
||||
explore a specific event of this pallet
|
||||
|
||||
Available <EVENT>'s in the \"Balances\" pallet:
|
||||
"};
|
||||
assert_eq_start!(output, start);
|
||||
// check that invalid subcommands don't work:
|
||||
let output = run_against_file("pallet Balances abc123").await;
|
||||
assert!(output.is_err());
|
||||
// check that we can explore a certain call:
|
||||
let output = run_against_file("pallet Balances calls transfer_keep_alive")
|
||||
.await
|
||||
.unwrap()
|
||||
.strip_ansi();
|
||||
// Note: at some point we want to switch to new metadata in the artifacts folder which has e.g. transfer_keep_alive instead of transfer.
|
||||
let start = formatdoc! {"
|
||||
Usage:
|
||||
subxt explore pallet Balances calls transfer_keep_alive <SCALE_VALUE>
|
||||
construct the call by providing a valid argument
|
||||
"};
|
||||
assert_eq_start!(output, start);
|
||||
// check that we can see methods of a runtime api:
|
||||
let output = run_against_file("api metadata").await.unwrap().strip_ansi();
|
||||
|
||||
let start = formatdoc! {"
|
||||
Description:
|
||||
The `Metadata` api trait that returns metadata for the runtime.
|
||||
|
||||
Usage:
|
||||
subxt explore api Metadata <METHOD>
|
||||
explore a specific runtime api method
|
||||
|
||||
Available <METHOD>'s available for the \"Metadata\" runtime api:
|
||||
"};
|
||||
assert_eq_start!(output, start);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn insecure_urls_get_denied() {
|
||||
// Connection should work fine:
|
||||
run("--url wss://rpc.polkadot.io:443").await.unwrap();
|
||||
|
||||
// Errors, because the --allow-insecure is not set:
|
||||
assert!(
|
||||
run("--url ws://rpc.polkadot.io:443")
|
||||
.await
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("is not secure")
|
||||
);
|
||||
|
||||
// This checks, that we never prevent (insecure) requests to localhost, even if the `--allow-insecure` flag is not set.
|
||||
// It errors, because there is no node running locally, which results in the "Request error".
|
||||
assert!(
|
||||
run("--url ws://localhost")
|
||||
.await
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("Request error")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
use clap::Args;
|
||||
use color_eyre::eyre::eyre;
|
||||
use color_eyre::owo_colors::OwoColorize;
|
||||
use indoc::{formatdoc, writedoc};
|
||||
use scale_info::form::PortableForm;
|
||||
use scale_info::{PortableRegistry, Type, TypeDef, TypeDefVariant};
|
||||
use scale_value::{Composite, ValueDef};
|
||||
use std::str::FromStr;
|
||||
|
||||
use subxt::tx;
|
||||
use subxt::utils::H256;
|
||||
use subxt::{
|
||||
OfflineClient,
|
||||
config::SubstrateConfig,
|
||||
metadata::{Metadata, PalletMetadata},
|
||||
};
|
||||
|
||||
use crate::utils::{
|
||||
Indent, SyntaxHighlight, fields_composite_example, fields_description,
|
||||
parse_string_into_scale_value,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Args)]
|
||||
pub struct CallsSubcommand {
|
||||
call: Option<String>,
|
||||
#[clap(required = false)]
|
||||
trailing_args: Vec<String>,
|
||||
}
|
||||
|
||||
pub fn explore_calls(
|
||||
command: CallsSubcommand,
|
||||
pallet_metadata: PalletMetadata,
|
||||
metadata: &Metadata,
|
||||
output: &mut impl std::io::Write,
|
||||
) -> color_eyre::Result<()> {
|
||||
let pallet_name = pallet_metadata.name();
|
||||
|
||||
// get the enum that stores the possible calls:
|
||||
let (calls_enum_type_def, _calls_enum_type) =
|
||||
get_calls_enum_type(pallet_metadata, metadata.types())?;
|
||||
|
||||
let usage = || {
|
||||
let calls = calls_to_string(calls_enum_type_def, pallet_name);
|
||||
formatdoc! {"
|
||||
Usage:
|
||||
subxt explore pallet {pallet_name} calls <CALL>
|
||||
explore a specific call of this pallet
|
||||
|
||||
{calls}
|
||||
"}
|
||||
};
|
||||
|
||||
// if no call specified, show user the calls to choose from:
|
||||
let Some(call_name) = command.call else {
|
||||
writeln!(output, "{}", usage())?;
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
// if specified call is wrong, show user the calls to choose from (but this time as an error):
|
||||
let Some(call) = calls_enum_type_def
|
||||
.variants
|
||||
.iter()
|
||||
.find(|variant| variant.name.eq_ignore_ascii_case(&call_name))
|
||||
else {
|
||||
return Err(eyre!(
|
||||
"\"{call_name}\" call not found in \"{pallet_name}\" pallet!\n\n{}",
|
||||
usage()
|
||||
));
|
||||
};
|
||||
|
||||
// collect all the trailing arguments into a single string that is later into a scale_value::Value
|
||||
let trailing_args = command.trailing_args.join(" ");
|
||||
|
||||
// if no trailing arguments specified show user the expected type of arguments with examples:
|
||||
if trailing_args.is_empty() {
|
||||
let fields: Vec<(Option<&str>, u32)> = call
|
||||
.fields
|
||||
.iter()
|
||||
.map(|f| (f.name.as_deref(), f.ty.id))
|
||||
.collect();
|
||||
let type_description = fields_description(&fields, &call.name, metadata.types()).indent(4);
|
||||
let fields_example =
|
||||
fields_composite_example(call.fields.iter().map(|e| e.ty.id), metadata.types())
|
||||
.indent(4)
|
||||
.highlight();
|
||||
|
||||
let scale_value_placeholder = "<SCALE_VALUE>".blue();
|
||||
|
||||
writedoc! {output, "
|
||||
Usage:
|
||||
subxt explore pallet {pallet_name} calls {call_name} {scale_value_placeholder}
|
||||
construct the call by providing a valid argument
|
||||
|
||||
The call expects a {scale_value_placeholder} with this shape:
|
||||
{type_description}
|
||||
|
||||
For example you could provide this {scale_value_placeholder}:
|
||||
{fields_example}
|
||||
"}?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// parse scale_value from trailing arguments and try to create an unsigned extrinsic with it:
|
||||
let value = parse_string_into_scale_value(&trailing_args)?;
|
||||
let value_as_composite = value_into_composite(value);
|
||||
let offline_client = mocked_offline_client(metadata.clone());
|
||||
let payload = tx::dynamic(pallet_name, call_name, value_as_composite);
|
||||
let unsigned_extrinsic = offline_client.tx().create_unsigned(&payload)?;
|
||||
let hex_bytes = format!("0x{}", hex::encode(unsigned_extrinsic.encoded()));
|
||||
writedoc! {output, "
|
||||
Encoded call data:
|
||||
{hex_bytes}
|
||||
"}?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn calls_to_string(pallet_calls: &TypeDefVariant<PortableForm>, pallet_name: &str) -> String {
|
||||
if pallet_calls.variants.is_empty() {
|
||||
return format!("No <CALL>'s available in the \"{pallet_name}\" pallet.");
|
||||
}
|
||||
let mut output = format!("Available <CALL>'s in the \"{pallet_name}\" pallet:");
|
||||
|
||||
let mut strings: Vec<_> = pallet_calls.variants.iter().map(|c| &c.name).collect();
|
||||
strings.sort();
|
||||
for variant in strings {
|
||||
output.push_str("\n ");
|
||||
output.push_str(variant);
|
||||
}
|
||||
output
|
||||
}
|
||||
|
||||
fn get_calls_enum_type<'a>(
|
||||
pallet: PalletMetadata,
|
||||
registry: &'a PortableRegistry,
|
||||
) -> color_eyre::Result<(&'a TypeDefVariant<PortableForm>, &'a Type<PortableForm>)> {
|
||||
let call_ty = pallet
|
||||
.call_ty_id()
|
||||
.ok_or(eyre!("The \"{}\" pallet has no calls.", pallet.name()))?;
|
||||
let calls_enum_type = registry
|
||||
.resolve(call_ty)
|
||||
.ok_or(eyre!("calls type with id {} not found.", call_ty))?;
|
||||
|
||||
// should always be a variant type, where each variant corresponds to one call.
|
||||
let TypeDef::Variant(calls_enum_type_def) = &calls_enum_type.type_def else {
|
||||
return Err(eyre!("calls type is not a variant"));
|
||||
};
|
||||
Ok((calls_enum_type_def, calls_enum_type))
|
||||
}
|
||||
|
||||
/// The specific values used for construction do not matter too much, we just need any OfflineClient to create unsigned extrinsics
|
||||
fn mocked_offline_client(metadata: Metadata) -> OfflineClient<SubstrateConfig> {
|
||||
let genesis_hash =
|
||||
H256::from_str("91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3")
|
||||
.expect("Valid hash; qed");
|
||||
|
||||
let runtime_version = subxt::client::RuntimeVersion {
|
||||
spec_version: 9370,
|
||||
transaction_version: 20,
|
||||
};
|
||||
|
||||
OfflineClient::<SubstrateConfig>::new(genesis_hash, runtime_version, metadata)
|
||||
}
|
||||
|
||||
/// composites stay composites, all other types are converted into a 1-fielded unnamed composite
|
||||
fn value_into_composite(value: scale_value::Value) -> scale_value::Composite<()> {
|
||||
match value.value {
|
||||
ValueDef::Composite(composite) => composite,
|
||||
_ => Composite::Unnamed(vec![value]),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
use clap::Args;
|
||||
use color_eyre::eyre::eyre;
|
||||
use indoc::{formatdoc, writedoc};
|
||||
use scale_typegen_description::type_description;
|
||||
use subxt::metadata::{Metadata, PalletMetadata};
|
||||
|
||||
use crate::utils::{Indent, SyntaxHighlight, first_paragraph_of_docs, format_scale_value};
|
||||
|
||||
#[derive(Debug, Clone, Args)]
|
||||
pub struct ConstantsSubcommand {
|
||||
constant: Option<String>,
|
||||
}
|
||||
|
||||
pub fn explore_constants(
|
||||
command: ConstantsSubcommand,
|
||||
pallet_metadata: PalletMetadata,
|
||||
metadata: &Metadata,
|
||||
output: &mut impl std::io::Write,
|
||||
) -> color_eyre::Result<()> {
|
||||
let pallet_name = pallet_metadata.name();
|
||||
|
||||
let usage = || {
|
||||
let constants = constants_to_string(pallet_metadata, pallet_name);
|
||||
formatdoc! {"
|
||||
Usage:
|
||||
subxt explore pallet {pallet_name} constants <CONSTANT>
|
||||
explore a specific constant of this pallet
|
||||
|
||||
{constants}
|
||||
"}
|
||||
};
|
||||
|
||||
let Some(constant_name) = command.constant else {
|
||||
writeln!(output, "{}", usage())?;
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
// if specified constant is wrong, show user the constants to choose from (but this time as an error):
|
||||
let Some(constant) = pallet_metadata
|
||||
.constants()
|
||||
.find(|constant| constant.name().eq_ignore_ascii_case(&constant_name))
|
||||
else {
|
||||
let err = eyre!(
|
||||
"constant \"{constant_name}\" not found in \"{pallet_name}\" pallet!\n\n{}",
|
||||
usage()
|
||||
);
|
||||
return Err(err);
|
||||
};
|
||||
|
||||
// docs
|
||||
let doc_string = first_paragraph_of_docs(constant.docs()).indent(4);
|
||||
if !doc_string.is_empty() {
|
||||
writedoc! {output, "
|
||||
Description:
|
||||
{doc_string}
|
||||
|
||||
"}?;
|
||||
}
|
||||
|
||||
// shape
|
||||
let type_description = type_description(constant.ty(), metadata.types(), true)
|
||||
.expect("No Type Description")
|
||||
.indent(4)
|
||||
.highlight();
|
||||
|
||||
// value
|
||||
let value =
|
||||
scale_value::scale::decode_as_type(&mut constant.value(), constant.ty(), metadata.types())?;
|
||||
let value = format_scale_value(&value).indent(4);
|
||||
|
||||
writedoc!(
|
||||
output,
|
||||
"
|
||||
The constant has the following shape:
|
||||
{type_description}
|
||||
|
||||
The value of the constant is:
|
||||
{value}
|
||||
"
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn constants_to_string(pallet_metadata: PalletMetadata, pallet_name: &str) -> String {
|
||||
if pallet_metadata.constants().len() == 0 {
|
||||
return format!("No <CONSTANT>'s available in the \"{pallet_name}\" pallet.");
|
||||
}
|
||||
let mut output = format!("Available <CONSTANT>'s in the \"{pallet_name}\" pallet:");
|
||||
let mut strings: Vec<_> = pallet_metadata.constants().map(|c| c.name()).collect();
|
||||
strings.sort();
|
||||
for constant in strings {
|
||||
output.push_str("\n ");
|
||||
output.push_str(constant);
|
||||
}
|
||||
output
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
use clap::Args;
|
||||
use color_eyre::eyre::eyre;
|
||||
use indoc::{formatdoc, writedoc};
|
||||
use scale_info::{Variant, form::PortableForm};
|
||||
use subxt::metadata::{Metadata, PalletMetadata};
|
||||
|
||||
use crate::utils::{Indent, fields_description, first_paragraph_of_docs};
|
||||
|
||||
#[derive(Debug, Clone, Args)]
|
||||
pub struct EventsSubcommand {
|
||||
event: Option<String>,
|
||||
}
|
||||
|
||||
pub fn explore_events(
|
||||
command: EventsSubcommand,
|
||||
pallet_metadata: PalletMetadata,
|
||||
metadata: &Metadata,
|
||||
output: &mut impl std::io::Write,
|
||||
) -> color_eyre::Result<()> {
|
||||
let pallet_name = pallet_metadata.name();
|
||||
let event_variants = pallet_metadata.event_variants().unwrap_or(&[]);
|
||||
|
||||
let usage = || {
|
||||
let events = events_to_string(event_variants, pallet_name);
|
||||
formatdoc! {"
|
||||
Usage:
|
||||
subxt explore pallet {pallet_name} events <EVENT>
|
||||
explore a specific event of this pallet
|
||||
|
||||
{events}
|
||||
"}
|
||||
};
|
||||
|
||||
let Some(event_name) = command.event else {
|
||||
writeln!(output, "{}", usage())?;
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
// if specified event is wrong, show user the events to choose from (but this time as an error):
|
||||
let Some(event) = event_variants
|
||||
.iter()
|
||||
.find(|event| event.name.eq_ignore_ascii_case(&event_name))
|
||||
else {
|
||||
let err = eyre!(
|
||||
"event \"{event_name}\" not found in \"{pallet_name}\" pallet!\n\n{}",
|
||||
usage()
|
||||
);
|
||||
return Err(err);
|
||||
};
|
||||
|
||||
let doc_string = first_paragraph_of_docs(&event.docs).indent(4);
|
||||
if !doc_string.is_empty() {
|
||||
writedoc! {output, "
|
||||
Description:
|
||||
{doc_string}
|
||||
|
||||
"}?;
|
||||
}
|
||||
|
||||
let fields: Vec<(Option<&str>, u32)> = event
|
||||
.fields
|
||||
.iter()
|
||||
.map(|f| (f.name.as_deref(), f.ty.id))
|
||||
.collect();
|
||||
let type_description = fields_description(&fields, &event.name, metadata.types()).indent(4);
|
||||
writedoc!(
|
||||
output,
|
||||
"
|
||||
The event has the following shape:
|
||||
{type_description}
|
||||
"
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn events_to_string(event_variants: &[Variant<PortableForm>], pallet_name: &str) -> String {
|
||||
if event_variants.is_empty() {
|
||||
return format!("No <EVENT>'s available in the \"{pallet_name}\" pallet.");
|
||||
}
|
||||
let mut output = format!("Available <EVENT>'s in the \"{pallet_name}\" pallet:");
|
||||
let mut strings: Vec<_> = event_variants.iter().map(|c| &c.name).collect();
|
||||
strings.sort();
|
||||
for event in strings {
|
||||
output.push_str("\n ");
|
||||
output.push_str(event);
|
||||
}
|
||||
output
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
use clap::Subcommand;
|
||||
|
||||
use indoc::writedoc;
|
||||
use subxt::Metadata;
|
||||
use pezkuwi_subxt_metadata::PalletMetadata;
|
||||
|
||||
use crate::utils::{FileOrUrl, Indent, first_paragraph_of_docs};
|
||||
|
||||
use self::{
|
||||
calls::CallsSubcommand,
|
||||
constants::ConstantsSubcommand,
|
||||
events::{EventsSubcommand, explore_events},
|
||||
storage::StorageSubcommand,
|
||||
};
|
||||
|
||||
use calls::explore_calls;
|
||||
use constants::explore_constants;
|
||||
use storage::explore_storage;
|
||||
|
||||
mod calls;
|
||||
mod constants;
|
||||
mod events;
|
||||
mod storage;
|
||||
|
||||
#[derive(Debug, Clone, Subcommand)]
|
||||
pub enum PalletSubcommand {
|
||||
Calls(CallsSubcommand),
|
||||
Constants(ConstantsSubcommand),
|
||||
Storage(StorageSubcommand),
|
||||
Events(EventsSubcommand),
|
||||
}
|
||||
|
||||
pub async fn run<'a>(
|
||||
subcommand: Option<PalletSubcommand>,
|
||||
pallet_metadata: PalletMetadata<'a>,
|
||||
metadata: &'a Metadata,
|
||||
file_or_url: FileOrUrl,
|
||||
output: &mut impl std::io::Write,
|
||||
) -> color_eyre::Result<()> {
|
||||
let pallet_name = pallet_metadata.name();
|
||||
let Some(subcommand) = subcommand else {
|
||||
let docs_string = first_paragraph_of_docs(pallet_metadata.docs()).indent(4);
|
||||
if !docs_string.is_empty() {
|
||||
writedoc! {output, "
|
||||
Description:
|
||||
{docs_string}
|
||||
|
||||
"}?;
|
||||
}
|
||||
|
||||
writedoc! {output, "
|
||||
Usage:
|
||||
subxt explore pallet {pallet_name} calls
|
||||
explore the calls that can be made into a pallet
|
||||
subxt explore pallet {pallet_name} constants
|
||||
explore the constants of a pallet
|
||||
subxt explore pallet {pallet_name} storage
|
||||
explore the storage values of a pallet
|
||||
subxt explore pallet {pallet_name} events
|
||||
explore the events of a pallet
|
||||
"}?;
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
match subcommand {
|
||||
PalletSubcommand::Calls(command) => {
|
||||
explore_calls(command, pallet_metadata, metadata, output)
|
||||
}
|
||||
PalletSubcommand::Constants(command) => {
|
||||
explore_constants(command, pallet_metadata, metadata, output)
|
||||
}
|
||||
PalletSubcommand::Storage(command) => {
|
||||
// if the metadata came from some url, we use that same url to make storage calls against.
|
||||
explore_storage(command, pallet_metadata, metadata, file_or_url, output).await
|
||||
}
|
||||
PalletSubcommand::Events(command) => {
|
||||
explore_events(command, pallet_metadata, metadata, output)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
use clap::Args;
|
||||
use color_eyre::{eyre::bail, owo_colors::OwoColorize};
|
||||
use indoc::{formatdoc, writedoc};
|
||||
use scale_typegen_description::type_description;
|
||||
use scale_value::Value;
|
||||
use std::fmt::Write;
|
||||
use std::write;
|
||||
use subxt::metadata::{Metadata, PalletMetadata, StorageMetadata};
|
||||
|
||||
use crate::utils::{
|
||||
FileOrUrl, Indent, SyntaxHighlight, create_client, first_paragraph_of_docs,
|
||||
parse_string_into_scale_value, type_example,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Args)]
|
||||
pub struct StorageSubcommand {
|
||||
storage_entry: Option<String>,
|
||||
#[clap(long, short, action)]
|
||||
execute: bool,
|
||||
#[clap(required = false)]
|
||||
trailing_args: Vec<String>,
|
||||
}
|
||||
|
||||
pub async fn explore_storage(
|
||||
command: StorageSubcommand,
|
||||
pallet_metadata: PalletMetadata<'_>,
|
||||
metadata: &Metadata,
|
||||
file_or_url: FileOrUrl,
|
||||
output: &mut impl std::io::Write,
|
||||
) -> color_eyre::Result<()> {
|
||||
let pallet_name = pallet_metadata.name();
|
||||
let trailing_args = command.trailing_args;
|
||||
|
||||
let Some(storage_metadata) = pallet_metadata.storage() else {
|
||||
writeln!(
|
||||
output,
|
||||
"The \"{pallet_name}\" pallet has no storage entries."
|
||||
)?;
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let storage_entry_placeholder = "<STORAGE_ENTRY>".blue();
|
||||
let usage = || {
|
||||
let storage_entries = storage_entries_string(storage_metadata, pallet_name);
|
||||
formatdoc! {"
|
||||
Usage:
|
||||
subxt explore pallet {pallet_name} storage {storage_entry_placeholder}
|
||||
explore a specific storage entry of this pallet
|
||||
|
||||
{storage_entries}
|
||||
"}
|
||||
};
|
||||
|
||||
// if no storage entry specified, show user the calls to choose from:
|
||||
let Some(entry_name) = command.storage_entry else {
|
||||
writeln!(output, "{}", usage())?;
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
// if specified call storage entry wrong, show user the storage entries to choose from (but this time as an error):
|
||||
let Some(storage) = storage_metadata
|
||||
.entries()
|
||||
.iter()
|
||||
.find(|entry| entry.name().eq_ignore_ascii_case(&entry_name))
|
||||
else {
|
||||
bail!(
|
||||
"Storage entry \"{entry_name}\" not found in \"{pallet_name}\" pallet!\n\n{}",
|
||||
usage()
|
||||
);
|
||||
};
|
||||
|
||||
let return_ty_id = storage.value_ty();
|
||||
|
||||
let key_value_placeholder = "<KEY_VALUE>".blue();
|
||||
|
||||
let docs_string = first_paragraph_of_docs(storage.docs()).indent(4);
|
||||
if !docs_string.is_empty() {
|
||||
writedoc! {output, "
|
||||
Description:
|
||||
{docs_string}
|
||||
|
||||
"}?;
|
||||
}
|
||||
|
||||
// only inform user about usage if `execute` flag not provided
|
||||
if !command.execute {
|
||||
writedoc! {output, "
|
||||
Usage:
|
||||
subxt explore pallet {pallet_name} storage {entry_name} --execute {key_value_placeholder}
|
||||
retrieve a value from storage
|
||||
|
||||
"}?;
|
||||
}
|
||||
|
||||
let return_ty_description = type_description(return_ty_id, metadata.types(), true)
|
||||
.expect("No type Description")
|
||||
.indent(4)
|
||||
.highlight();
|
||||
|
||||
writedoc! {output, "
|
||||
The storage entry has the following shape:
|
||||
{return_ty_description}
|
||||
"}?;
|
||||
|
||||
// inform user about shape of the key if it can be provided:
|
||||
let storage_keys = storage.keys().collect::<Vec<_>>();
|
||||
if !storage_keys.is_empty() {
|
||||
let key_ty_description = format!(
|
||||
"({})",
|
||||
storage_keys
|
||||
.iter()
|
||||
.map(|key| type_description(key.key_id, metadata.types(), true)
|
||||
.expect("No type Description"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
)
|
||||
.indent(4)
|
||||
.highlight();
|
||||
|
||||
let key_ty_example = format!(
|
||||
"({})",
|
||||
storage_keys
|
||||
.iter()
|
||||
.map(|key| type_example(key.key_id, metadata.types()).to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
)
|
||||
.indent(4)
|
||||
.highlight();
|
||||
|
||||
writedoc! {output, "
|
||||
|
||||
The {key_value_placeholder} has the following shape:
|
||||
{key_ty_description}
|
||||
|
||||
For example you could provide this {key_value_placeholder}:
|
||||
{key_ty_example}
|
||||
"}?;
|
||||
} else {
|
||||
writedoc! {output,"
|
||||
|
||||
Can be accessed without providing a {key_value_placeholder}.
|
||||
"}?;
|
||||
}
|
||||
|
||||
// if `--execute`/`-e` flag is set, try to execute the storage entry request
|
||||
if !command.execute {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let storage_entry_keys: Vec<Value> = match (!trailing_args.is_empty(), !storage_keys.is_empty())
|
||||
{
|
||||
// keys provided, keys not needed.
|
||||
(true, false) => {
|
||||
let trailing_args_str = trailing_args.join(" ");
|
||||
let warning = format!(
|
||||
"Warning: You submitted one or more keys \"{trailing_args_str}\", but no key is needed. To access the storage value, please do not provide any keys."
|
||||
);
|
||||
writeln!(output, "{}", warning.yellow())?;
|
||||
return Ok(());
|
||||
}
|
||||
// Keys not provided, keys needed.
|
||||
(false, true) => {
|
||||
// just return. The user was instructed above how to provide a value if they want to.
|
||||
return Ok(());
|
||||
}
|
||||
// Keys not provided, keys not needed.
|
||||
(false, false) => vec![],
|
||||
// Keys provided, keys needed.
|
||||
(true, true) => {
|
||||
// Each trailing arg is parsed into its own value, to be provided as a separate storage key.
|
||||
let values = trailing_args
|
||||
.iter()
|
||||
.map(|arg| parse_string_into_scale_value(arg))
|
||||
.collect::<color_eyre::Result<Vec<_>>>()?;
|
||||
|
||||
// We do this just to print them out.
|
||||
let values_str = values
|
||||
.iter()
|
||||
.map(|v| v.to_string().highlight())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
let value_str = values_str.indent(4);
|
||||
|
||||
writedoc! {output, "
|
||||
|
||||
You submitted the following {key_value_placeholder}:
|
||||
{value_str}
|
||||
"}?;
|
||||
|
||||
values
|
||||
}
|
||||
};
|
||||
|
||||
// construct the client:
|
||||
let client = create_client(&file_or_url).await?;
|
||||
|
||||
// Fetch the value:
|
||||
let storage_value = client
|
||||
.storage()
|
||||
.at_latest()
|
||||
.await?
|
||||
.fetch((pallet_name, storage.name()), storage_entry_keys)
|
||||
.await?
|
||||
.decode()?;
|
||||
|
||||
let value = storage_value.to_string().highlight();
|
||||
|
||||
writedoc! {output, "
|
||||
|
||||
The value of the storage entry is:
|
||||
{value}
|
||||
"}?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn storage_entries_string(storage_metadata: &StorageMetadata, pallet_name: &str) -> String {
|
||||
let storage_entry_placeholder = "<STORAGE_ENTRY>".blue();
|
||||
if storage_metadata.entries().is_empty() {
|
||||
format!("No {storage_entry_placeholder}'s available in the \"{pallet_name}\" pallet.")
|
||||
} else {
|
||||
let mut output =
|
||||
format!("Available {storage_entry_placeholder}'s in the \"{pallet_name}\" pallet:");
|
||||
let mut strings: Vec<_> = storage_metadata
|
||||
.entries()
|
||||
.iter()
|
||||
.map(|s| s.name())
|
||||
.collect();
|
||||
strings.sort();
|
||||
for entry in strings {
|
||||
write!(output, "\n {entry}").unwrap();
|
||||
}
|
||||
output
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
use crate::utils::{
|
||||
FileOrUrl, Indent, SyntaxHighlight, create_client, fields_composite_example,
|
||||
fields_description, first_paragraph_of_docs, parse_string_into_scale_value,
|
||||
};
|
||||
|
||||
use color_eyre::{
|
||||
eyre::{bail, eyre},
|
||||
owo_colors::OwoColorize,
|
||||
};
|
||||
|
||||
use indoc::{formatdoc, writedoc};
|
||||
use scale_typegen_description::type_description;
|
||||
use scale_value::Value;
|
||||
use subxt::{
|
||||
Metadata,
|
||||
ext::{scale_decode::DecodeAsType, scale_encode::EncodeAsType},
|
||||
};
|
||||
use pezkuwi_subxt_metadata::RuntimeApiMetadata;
|
||||
|
||||
/// Runs for a specified runtime API trait.
|
||||
/// Cases to consider:
|
||||
/// ```text
|
||||
/// method is:
|
||||
/// None => Show pallet docs + available methods
|
||||
/// Some (invalid) => Show Error + available methods
|
||||
/// Some (valid) => Show method docs + output type description
|
||||
/// execute is:
|
||||
/// false => Show input type description + Example Value
|
||||
/// true => validate (trailing args + build node connection)
|
||||
/// validation is:
|
||||
/// Err => Show Error
|
||||
/// Ok => Make a runtime api call with the provided args.
|
||||
/// response is:
|
||||
/// Err => Show Error
|
||||
/// Ok => Show the result
|
||||
/// ```
|
||||
pub async fn run<'a>(
|
||||
method: Option<String>,
|
||||
execute: bool,
|
||||
trailing_args: Vec<String>,
|
||||
runtime_api_metadata: RuntimeApiMetadata<'a>,
|
||||
metadata: &'a Metadata,
|
||||
file_or_url: FileOrUrl,
|
||||
output: &mut impl std::io::Write,
|
||||
) -> color_eyre::Result<()> {
|
||||
let api_name = runtime_api_metadata.name();
|
||||
|
||||
let usage = || {
|
||||
let methods = methods_to_string(&runtime_api_metadata);
|
||||
formatdoc! {"
|
||||
Usage:
|
||||
subxt explore api {api_name} <METHOD>
|
||||
explore a specific runtime api method
|
||||
|
||||
{methods}
|
||||
"}
|
||||
};
|
||||
|
||||
// If method is None: Show pallet docs + available methods
|
||||
let Some(method_name) = method else {
|
||||
let doc_string = first_paragraph_of_docs(runtime_api_metadata.docs()).indent(4);
|
||||
if !doc_string.is_empty() {
|
||||
writedoc! {output, "
|
||||
Description:
|
||||
{doc_string}
|
||||
|
||||
"}?;
|
||||
}
|
||||
writeln!(output, "{}", usage())?;
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
// If method is invalid: Show Error + available methods
|
||||
let Some(method) = runtime_api_metadata
|
||||
.methods()
|
||||
.find(|e| e.name().eq_ignore_ascii_case(&method_name))
|
||||
else {
|
||||
return Err(eyre!(
|
||||
"\"{method_name}\" method not found for \"{method_name}\" runtime api!\n\n{}",
|
||||
usage()
|
||||
));
|
||||
};
|
||||
// redeclare to not use the wrong capitalization of the input from here on:
|
||||
let method_name = method.name();
|
||||
|
||||
// Method is valid. Show method docs + output type description
|
||||
let doc_string = first_paragraph_of_docs(method.docs()).indent(4);
|
||||
if !doc_string.is_empty() {
|
||||
writedoc! {output, "
|
||||
Description:
|
||||
{doc_string}
|
||||
|
||||
"}?;
|
||||
}
|
||||
|
||||
let input_value_placeholder = "<INPUT_VALUE>".blue();
|
||||
|
||||
// Output type description
|
||||
let input_values = || {
|
||||
if method.inputs().len() == 0 {
|
||||
return format!("The method does not require an {input_value_placeholder}");
|
||||
}
|
||||
|
||||
let fields: Vec<(Option<&str>, u32)> =
|
||||
method.inputs().map(|f| (Some(&*f.name), f.id)).collect();
|
||||
let fields_description =
|
||||
fields_description(&fields, method.name(), metadata.types()).indent(4);
|
||||
|
||||
let fields_example =
|
||||
fields_composite_example(method.inputs().map(|e| e.id), metadata.types())
|
||||
.indent(4)
|
||||
.highlight();
|
||||
|
||||
formatdoc! {"
|
||||
The method expects an {input_value_placeholder} with this shape:
|
||||
{fields_description}
|
||||
|
||||
For example you could provide this {input_value_placeholder}:
|
||||
{fields_example}"}
|
||||
};
|
||||
|
||||
let execute_usage = || {
|
||||
let output = type_description(method.output_ty(), metadata.types(), true)
|
||||
.expect("No Type Description")
|
||||
.indent(4)
|
||||
.highlight();
|
||||
let input = input_values();
|
||||
formatdoc! {"
|
||||
Usage:
|
||||
subxt explore api {api_name} {method_name} --execute {input_value_placeholder}
|
||||
make a runtime api request
|
||||
|
||||
The Output of this method has the following shape:
|
||||
{output}
|
||||
|
||||
{input}"}
|
||||
};
|
||||
|
||||
writeln!(output, "{}", execute_usage())?;
|
||||
if !execute {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if trailing_args.len() != method.inputs().len() {
|
||||
bail!(
|
||||
"The number of trailing arguments you provided after the `execute` flag does not match the expected number of inputs!\n{}",
|
||||
execute_usage()
|
||||
);
|
||||
}
|
||||
|
||||
// encode each provided input as bytes of the correct type:
|
||||
let args_data: Vec<Value> = method
|
||||
.inputs()
|
||||
.zip(trailing_args.iter())
|
||||
.map(|(ty, arg)| {
|
||||
let value = parse_string_into_scale_value(arg)?;
|
||||
let value_str = value.indent(4);
|
||||
// convert to bytes:
|
||||
writedoc! {output, "
|
||||
|
||||
You submitted the following {input_value_placeholder}:
|
||||
{value_str}
|
||||
"}?;
|
||||
// encode, then decode. This ensures that the scale value is of the correct shape for the param:
|
||||
let bytes = value.encode_as_type(ty.id, metadata.types())?;
|
||||
let value = Value::decode_as_type(&mut &bytes[..], ty.id, metadata.types())?;
|
||||
Ok(value)
|
||||
})
|
||||
.collect::<color_eyre::Result<Vec<Value>>>()?;
|
||||
|
||||
let method_call =
|
||||
subxt::dynamic::runtime_api_call::<_, Value>(api_name, method.name(), args_data);
|
||||
let client = create_client(&file_or_url).await?;
|
||||
let output_value = client
|
||||
.runtime_api()
|
||||
.at_latest()
|
||||
.await?
|
||||
.call(method_call)
|
||||
.await?;
|
||||
|
||||
let output_value = output_value.to_string().highlight();
|
||||
writedoc! {output, "
|
||||
|
||||
Returned value:
|
||||
{output_value}
|
||||
"}?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn methods_to_string(runtime_api_metadata: &RuntimeApiMetadata<'_>) -> String {
|
||||
let api_name = runtime_api_metadata.name();
|
||||
if runtime_api_metadata.methods().len() == 0 {
|
||||
return format!("No <METHOD>'s available for the \"{api_name}\" runtime api.");
|
||||
}
|
||||
|
||||
let mut output = format!("Available <METHOD>'s available for the \"{api_name}\" runtime api:");
|
||||
let mut strings: Vec<_> = runtime_api_metadata.methods().map(|e| e.name()).collect();
|
||||
strings.sort();
|
||||
for variant in strings {
|
||||
output.push_str("\n ");
|
||||
output.push_str(variant);
|
||||
}
|
||||
output
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
use crate::utils::{FileOrUrl, validate_url_security};
|
||||
use clap::Parser as ClapParser;
|
||||
use codec::{Decode, Encode};
|
||||
use color_eyre::eyre::{self, bail};
|
||||
use frame_metadata::{RuntimeMetadata, RuntimeMetadataPrefixed};
|
||||
use std::{io::Write, path::PathBuf};
|
||||
use pezkuwi_subxt_utils_stripmetadata::StripMetadata;
|
||||
|
||||
/// Download metadata from a substrate node, for use with `subxt` codegen.
|
||||
#[derive(Debug, ClapParser)]
|
||||
pub struct Opts {
|
||||
#[command(flatten)]
|
||||
file_or_url: FileOrUrl,
|
||||
/// The format of the metadata to display: `json`, `hex` or `bytes`.
|
||||
#[clap(long, short, default_value = "bytes")]
|
||||
format: String,
|
||||
/// Generate a subset of the metadata that contains only the
|
||||
/// types needed to represent the provided pallets.
|
||||
///
|
||||
/// The returned metadata is updated to the latest available version
|
||||
/// when using the option.
|
||||
#[clap(long, use_value_delimiter = true, value_parser)]
|
||||
pallets: Option<Vec<String>>,
|
||||
/// Generate a subset of the metadata that contains only the
|
||||
/// runtime APIs needed.
|
||||
///
|
||||
/// The returned metadata is updated to the latest available version
|
||||
/// when using the option.
|
||||
#[clap(long, use_value_delimiter = true, value_parser)]
|
||||
runtime_apis: Option<Vec<String>>,
|
||||
/// Write the output of the metadata command to the provided file path.
|
||||
#[clap(long, short, value_parser)]
|
||||
pub output_file: Option<PathBuf>,
|
||||
/// Allow insecure URLs e.g. URLs starting with ws:// or http:// without SSL encryption
|
||||
#[clap(long, short)]
|
||||
allow_insecure: bool,
|
||||
}
|
||||
|
||||
pub async fn run(opts: Opts, output: &mut impl Write) -> color_eyre::Result<()> {
|
||||
validate_url_security(opts.file_or_url.url.as_ref(), opts.allow_insecure)?;
|
||||
let bytes = opts.file_or_url.fetch().await?;
|
||||
|
||||
let mut metadata = RuntimeMetadataPrefixed::decode(&mut &bytes[..])?;
|
||||
|
||||
// Strip pallets or runtime APIs if names are provided:
|
||||
if opts.pallets.is_some() || opts.runtime_apis.is_some() {
|
||||
let keep_pallets_fn: Box<dyn Fn(&str) -> bool> = match opts.pallets.as_ref() {
|
||||
Some(pallets) => Box::new(|name| pallets.iter().any(|p| &**p == name)),
|
||||
None => Box::new(|_| true),
|
||||
};
|
||||
let keep_runtime_apis_fn: Box<dyn Fn(&str) -> bool> = match opts.runtime_apis.as_ref() {
|
||||
Some(apis) => Box::new(|name| apis.iter().any(|p| &**p == name)),
|
||||
None => Box::new(|_| true),
|
||||
};
|
||||
|
||||
match &mut metadata.1 {
|
||||
RuntimeMetadata::V14(md) => md.strip_metadata(keep_pallets_fn, keep_runtime_apis_fn),
|
||||
RuntimeMetadata::V15(md) => md.strip_metadata(keep_pallets_fn, keep_runtime_apis_fn),
|
||||
RuntimeMetadata::V16(md) => md.strip_metadata(keep_pallets_fn, keep_runtime_apis_fn),
|
||||
_ => {
|
||||
bail!(
|
||||
"Unsupported metadata version for stripping pallets/runtime APIs: V14, V15 or V16 metadata is expected."
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut output: Box<dyn Write> = match opts.output_file {
|
||||
Some(path) => Box::new(std::fs::File::create(path)?),
|
||||
None => Box::new(output),
|
||||
};
|
||||
|
||||
match opts.format.as_str() {
|
||||
"json" => {
|
||||
let json = serde_json::to_string_pretty(&metadata)?;
|
||||
write!(output, "{json}")?;
|
||||
Ok(())
|
||||
}
|
||||
"hex" => {
|
||||
let hex_data = format!("0x{}", hex::encode(metadata.encode()));
|
||||
write!(output, "{hex_data}")?;
|
||||
Ok(())
|
||||
}
|
||||
"bytes" => {
|
||||
let bytes = metadata.encode();
|
||||
output.write_all(&bytes)?;
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(eyre::eyre!(
|
||||
"Unsupported format `{}`, expected `json`, `hex` or `bytes`",
|
||||
opts.format
|
||||
)),
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
pub mod chain_spec;
|
||||
pub mod codegen;
|
||||
pub mod compatibility;
|
||||
pub mod diff;
|
||||
pub mod explore;
|
||||
pub mod metadata;
|
||||
pub mod version;
|
||||
@@ -0,0 +1,17 @@
|
||||
use clap::Parser as ClapParser;
|
||||
|
||||
/// Prints version information
|
||||
#[derive(Debug, ClapParser)]
|
||||
pub struct Opts {}
|
||||
|
||||
pub fn run(_opts: Opts, output: &mut impl std::io::Write) -> color_eyre::Result<()> {
|
||||
let git_hash = env!("GIT_HASH");
|
||||
writeln!(
|
||||
output,
|
||||
"{} {}-{}",
|
||||
clap::crate_name!(),
|
||||
clap::crate_version!(),
|
||||
git_hash
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
Vendored
+38
@@ -0,0 +1,38 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! The Subxt CLI tool.
|
||||
|
||||
mod commands;
|
||||
mod utils;
|
||||
|
||||
use clap::Parser as ClapParser;
|
||||
|
||||
/// Subxt utilities for interacting with Substrate based nodes.
|
||||
#[derive(Debug, ClapParser)]
|
||||
enum Command {
|
||||
Metadata(commands::metadata::Opts),
|
||||
Codegen(commands::codegen::Opts),
|
||||
Compatibility(commands::compatibility::Opts),
|
||||
Diff(commands::diff::Opts),
|
||||
Version(commands::version::Opts),
|
||||
Explore(commands::explore::Opts),
|
||||
ChainSpec(commands::chain_spec::Opts),
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> color_eyre::Result<()> {
|
||||
color_eyre::install()?;
|
||||
let args = Command::parse();
|
||||
let mut output = std::io::stdout();
|
||||
match args {
|
||||
Command::Metadata(opts) => commands::metadata::run(opts, &mut output).await,
|
||||
Command::Codegen(opts) => commands::codegen::run(opts, &mut output).await,
|
||||
Command::Compatibility(opts) => commands::compatibility::run(opts, &mut output).await,
|
||||
Command::Diff(opts) => commands::diff::run(opts, &mut output).await,
|
||||
Command::Version(opts) => commands::version::run(opts, &mut output),
|
||||
Command::Explore(opts) => commands::explore::run(opts, &mut output).await,
|
||||
Command::ChainSpec(opts) => commands::chain_spec::run(opts, &mut output).await,
|
||||
}
|
||||
}
|
||||
+393
@@ -0,0 +1,393 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
use clap::Args;
|
||||
use color_eyre::eyre::{bail, eyre};
|
||||
use color_eyre::owo_colors::OwoColorize;
|
||||
use heck::ToUpperCamelCase;
|
||||
use scale_info::PortableRegistry;
|
||||
use scale_typegen_description::{format_type_description, type_description};
|
||||
use std::fmt::Display;
|
||||
use std::str::FromStr;
|
||||
use std::{fs, io::Read, path::PathBuf};
|
||||
use subxt::{OnlineClient, PolkadotConfig};
|
||||
|
||||
use scale_value::Value;
|
||||
use pezkuwi_subxt_utils_fetchmetadata::{self as fetch_metadata, MetadataVersion, Url};
|
||||
|
||||
/// The source of the metadata.
|
||||
#[derive(Debug, Args, Clone)]
|
||||
pub struct FileOrUrl {
|
||||
/// The url of the substrate node to query for metadata for codegen.
|
||||
#[clap(long, value_parser)]
|
||||
pub url: Option<Url>,
|
||||
/// The path to the encoded metadata file.
|
||||
#[clap(long, value_parser)]
|
||||
pub file: Option<PathOrStdIn>,
|
||||
/// Specify the metadata version.
|
||||
///
|
||||
/// - "latest": Use the latest stable version available.
|
||||
/// - "unstable": Use the unstable metadata, if present.
|
||||
/// - a number: Use a specific metadata version.
|
||||
///
|
||||
/// Defaults to asking for the latest stable metadata version.
|
||||
#[clap(long)]
|
||||
pub version: Option<MetadataVersion>,
|
||||
/// Block hash (hex encoded) to attempt to fetch the metadata from.
|
||||
/// If not provided, we default to the latest finalized block.
|
||||
/// Non-archive nodes will be unable to provide metadata from old blocks.
|
||||
#[clap(long)]
|
||||
pub at_block: Option<String>,
|
||||
}
|
||||
|
||||
impl FromStr for FileOrUrl {
|
||||
type Err = &'static str;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
if let Ok(path) = PathOrStdIn::from_str(s) {
|
||||
Ok(FileOrUrl {
|
||||
url: None,
|
||||
file: Some(path),
|
||||
version: None,
|
||||
at_block: None,
|
||||
})
|
||||
} else {
|
||||
Url::parse(s)
|
||||
.map_err(|_| "Parsing Path or Uri failed.")
|
||||
.map(|uri| FileOrUrl {
|
||||
url: Some(uri),
|
||||
file: None,
|
||||
version: None,
|
||||
at_block: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// If `--path -` is provided, read bytes for metadata from stdin
|
||||
const STDIN_PATH_NAME: &str = "-";
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum PathOrStdIn {
|
||||
Path(PathBuf),
|
||||
StdIn,
|
||||
}
|
||||
|
||||
impl FromStr for PathOrStdIn {
|
||||
type Err = &'static str;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let s = s.trim();
|
||||
if s == STDIN_PATH_NAME {
|
||||
Ok(PathOrStdIn::StdIn)
|
||||
} else {
|
||||
let path = std::path::Path::new(s);
|
||||
if path.exists() {
|
||||
Ok(PathOrStdIn::Path(PathBuf::from(path)))
|
||||
} else {
|
||||
Err("Path does not exist.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FileOrUrl {
|
||||
/// Fetch the metadata bytes.
|
||||
pub async fn fetch(&self) -> color_eyre::Result<Vec<u8>> {
|
||||
match (&self.file, &self.url, self.version, &self.at_block) {
|
||||
// Can't provide both --file and --url
|
||||
(Some(_), Some(_), _, _) => {
|
||||
bail!("specify one of `--url` or `--file` but not both")
|
||||
}
|
||||
// --at-block must be provided with --url
|
||||
(Some(_path_or_stdin), _, _, Some(_at_block)) => {
|
||||
bail!("`--at-block` can only be used with `--url`")
|
||||
}
|
||||
// Load from --file path
|
||||
(Some(PathOrStdIn::Path(path)), None, None, None) => {
|
||||
let mut file = fs::File::open(path)?;
|
||||
let mut bytes = Vec::new();
|
||||
file.read_to_end(&mut bytes)?;
|
||||
Ok(bytes)
|
||||
}
|
||||
(Some(PathOrStdIn::StdIn), None, None, None) => {
|
||||
let reader = std::io::BufReader::new(std::io::stdin());
|
||||
let res = reader.bytes().collect::<Result<Vec<u8>, _>>();
|
||||
|
||||
match res {
|
||||
Ok(bytes) => Ok(bytes),
|
||||
Err(err) => bail!("reading bytes from stdin (`--file -`) failed: {err}"),
|
||||
}
|
||||
}
|
||||
// Cannot load the metadata from the file and specify a version to fetch.
|
||||
(Some(_), None, Some(_), None) => {
|
||||
// Note: we could provide the ability to convert between metadata versions
|
||||
// but that would be involved because we'd need to convert
|
||||
// from each metadata to the latest one and from the
|
||||
// latest one to each metadata version. For now, disable the conversion.
|
||||
bail!("`--file` is incompatible with `--version`")
|
||||
}
|
||||
// Fetch from --url
|
||||
(None, Some(uri), version, at_block) => Ok(fetch_metadata::from_url(
|
||||
uri.clone(),
|
||||
version.unwrap_or_default(),
|
||||
at_block.as_deref(),
|
||||
)
|
||||
.await?),
|
||||
// Default if neither is provided; fetch from local url
|
||||
(None, None, version, at_block) => {
|
||||
let url = Url::parse("ws://localhost:9944").expect("Valid URL; qed");
|
||||
Ok(
|
||||
fetch_metadata::from_url(url, version.unwrap_or_default(), at_block.as_deref())
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// creates an example value for each of the fields and
|
||||
/// packages all of them into one unnamed composite value.
|
||||
pub fn fields_composite_example(
|
||||
fields: impl Iterator<Item = u32>,
|
||||
types: &PortableRegistry,
|
||||
) -> Value {
|
||||
let examples: Vec<Value> = fields.map(|e| type_example(e, types)).collect();
|
||||
Value::unnamed_composite(examples)
|
||||
}
|
||||
|
||||
/// Returns a field description that is already formatted.
|
||||
pub fn fields_description(
|
||||
fields: &[(Option<&str>, u32)],
|
||||
name: &str,
|
||||
types: &PortableRegistry,
|
||||
) -> String {
|
||||
if fields.is_empty() {
|
||||
return "Zero Sized Type, no fields.".to_string();
|
||||
}
|
||||
let all_named = fields.iter().all(|f| f.0.is_some());
|
||||
|
||||
let fields = fields
|
||||
.iter()
|
||||
.map(|field| {
|
||||
let field_description =
|
||||
type_description(field.1, types, false).expect("No Description.");
|
||||
if all_named {
|
||||
let field_name = field.0.unwrap();
|
||||
format!("{field_name}: {field_description}")
|
||||
} else {
|
||||
field_description.to_string()
|
||||
}
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join(",");
|
||||
|
||||
let name = name.to_upper_camel_case();
|
||||
let end_result = if all_named {
|
||||
format!("{name} {{{fields}}}")
|
||||
} else {
|
||||
format!("{name} ({fields})")
|
||||
};
|
||||
// end_result
|
||||
format_type_description(&end_result).highlight()
|
||||
}
|
||||
|
||||
pub fn format_scale_value<T>(value: &Value<T>) -> String {
|
||||
scale_typegen_description::format_type_description(&value.to_string()).highlight()
|
||||
}
|
||||
|
||||
pub fn type_example(type_id: u32, types: &PortableRegistry) -> Value {
|
||||
scale_typegen_description::scale_value_from_seed(type_id, types, time_based_seed()).expect("")
|
||||
}
|
||||
|
||||
fn time_based_seed() -> u64 {
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.expect("We should always live in the future.")
|
||||
.subsec_millis() as u64
|
||||
}
|
||||
|
||||
pub fn first_paragraph_of_docs(docs: &[String]) -> String {
|
||||
// take at most the first paragraph of documentation, such that it does not get too long.
|
||||
docs.iter()
|
||||
.map(|e| e.trim())
|
||||
.take_while(|e| !e.is_empty())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
pub trait Indent: ToString {
|
||||
fn indent(&self, indent: usize) -> String {
|
||||
let indent_str = " ".repeat(indent);
|
||||
self.to_string()
|
||||
.lines()
|
||||
.map(|line| format!("{indent_str}{line}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Display> Indent for T {}
|
||||
|
||||
pub async fn create_client(
|
||||
file_or_url: &FileOrUrl,
|
||||
) -> color_eyre::Result<OnlineClient<PolkadotConfig>> {
|
||||
let client = match &file_or_url.url {
|
||||
Some(url) => OnlineClient::<PolkadotConfig>::from_url(url).await?,
|
||||
None => OnlineClient::<PolkadotConfig>::new().await?,
|
||||
};
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
pub fn parse_string_into_scale_value(str: &str) -> color_eyre::Result<Value> {
|
||||
let value = scale_value::stringify::from_str(str).0.map_err(|err| {
|
||||
eyre!(
|
||||
"scale_value::stringify::from_str led to a ParseError.\n\ntried parsing: \"{str}\"\n\n{err}",
|
||||
)
|
||||
})?;
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
pub trait SyntaxHighlight {
|
||||
fn highlight(&self) -> String;
|
||||
}
|
||||
|
||||
impl<T: AsRef<str>> SyntaxHighlight for T {
|
||||
fn highlight(&self) -> String {
|
||||
let _e = 323.0;
|
||||
let mut output: String = String::new();
|
||||
let mut word: String = String::new();
|
||||
|
||||
let mut in_word: Option<InWord> = None;
|
||||
|
||||
for c in self.as_ref().chars() {
|
||||
match c {
|
||||
'{' | '}' | ',' | '(' | ')' | ':' | '<' | '>' | ' ' | '\n' | '[' | ']' | ';' => {
|
||||
// flush the current word:
|
||||
if let Some(is_word) = in_word {
|
||||
let word = if word == "enum" {
|
||||
word.blue().to_string()
|
||||
} else {
|
||||
is_word.colorize(&word)
|
||||
};
|
||||
output.push_str(&word);
|
||||
}
|
||||
|
||||
in_word = None;
|
||||
word.clear();
|
||||
// push the symbol itself:
|
||||
output.push(c);
|
||||
}
|
||||
l => {
|
||||
if in_word.is_none() {
|
||||
in_word = Some(InWord::from_first_char(l))
|
||||
}
|
||||
word.push(l);
|
||||
}
|
||||
}
|
||||
}
|
||||
// flush if ending on a word:
|
||||
if let Some(word_kind) = in_word {
|
||||
output.push_str(&word_kind.colorize(&word));
|
||||
}
|
||||
|
||||
return output;
|
||||
|
||||
enum InWord {
|
||||
Lower,
|
||||
Upper,
|
||||
Number,
|
||||
}
|
||||
|
||||
impl InWord {
|
||||
fn colorize(&self, str: &str) -> String {
|
||||
let color = match self {
|
||||
InWord::Lower => (156, 220, 254),
|
||||
InWord::Upper => (78, 201, 176),
|
||||
InWord::Number => (181, 206, 168),
|
||||
};
|
||||
str.truecolor(color.0, color.1, color.2).to_string()
|
||||
}
|
||||
|
||||
fn from_first_char(c: char) -> Self {
|
||||
if c.is_numeric() {
|
||||
Self::Number
|
||||
} else if c.is_uppercase() {
|
||||
Self::Upper
|
||||
} else {
|
||||
Self::Lower
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn validate_url_security(url: Option<&Url>, allow_insecure: bool) -> color_eyre::Result<()> {
|
||||
let Some(url) = url else {
|
||||
return Ok(());
|
||||
};
|
||||
match subxt::utils::url_is_secure(url.as_str()) {
|
||||
Ok(is_secure) => {
|
||||
if !allow_insecure && !is_secure {
|
||||
bail!(
|
||||
"URL {url} is not secure!\nIf you are really want to use this URL, try using --allow-insecure (-a)"
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
bail!("URL {url} is not valid: {err}")
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::utils::{FileOrUrl, PathOrStdIn};
|
||||
use std::str::FromStr;
|
||||
|
||||
#[test]
|
||||
fn parsing() {
|
||||
assert!(matches!(
|
||||
FileOrUrl::from_str("-"),
|
||||
Ok(FileOrUrl {
|
||||
url: None,
|
||||
file: Some(PathOrStdIn::StdIn),
|
||||
version: None,
|
||||
at_block: None,
|
||||
})
|
||||
),);
|
||||
|
||||
assert!(matches!(
|
||||
FileOrUrl::from_str(" - "),
|
||||
Ok(FileOrUrl {
|
||||
url: None,
|
||||
file: Some(PathOrStdIn::StdIn),
|
||||
version: None,
|
||||
at_block: None,
|
||||
})
|
||||
),);
|
||||
|
||||
assert!(matches!(
|
||||
FileOrUrl::from_str("./src/main.rs"),
|
||||
Ok(FileOrUrl {
|
||||
url: None,
|
||||
file: Some(PathOrStdIn::Path(_)),
|
||||
version: None,
|
||||
at_block: None,
|
||||
})
|
||||
),);
|
||||
|
||||
assert!(FileOrUrl::from_str("./src/i_dont_exist.rs").is_err());
|
||||
|
||||
assert!(matches!(
|
||||
FileOrUrl::from_str("https://github.com/paritytech/subxt"),
|
||||
Ok(FileOrUrl {
|
||||
url: Some(_),
|
||||
file: None,
|
||||
version: None,
|
||||
at_block: None,
|
||||
})
|
||||
));
|
||||
}
|
||||
}
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
# result_large_err lint complains if error variant is 128 bytes or more by default.
|
||||
# Our error is. Let's up this limit a bit for now to avoid lots of warnings.
|
||||
large-error-threshold = 512
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
[package]
|
||||
name = "pezkuwi-subxt-codegen"
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
publish = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
documentation = "https://docs.rs/pezkuwi-subxt-codegen"
|
||||
homepage.workspace = true
|
||||
description = "Generate an API for interacting with a Pezkuwi/Bizinikiwi node from FRAME metadata"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
web = ["getrandom/js"]
|
||||
|
||||
[dependencies]
|
||||
codec = { package = "parity-scale-codec", workspace = true, features = ["derive"] }
|
||||
frame-metadata = { workspace = true, optional = true }
|
||||
heck = { workspace = true }
|
||||
proc-macro2 = { workspace = true }
|
||||
quote = { workspace = true }
|
||||
syn = { workspace = true }
|
||||
scale-info = { workspace = true }
|
||||
pezkuwi-subxt-metadata = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
scale-typegen = { workspace = true }
|
||||
|
||||
# Included if "web" feature is enabled, to enable its js feature.
|
||||
getrandom = { workspace = true, optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
scale-info = { workspace = true, features = ["bit-vec"] }
|
||||
frame-metadata = { workspace = true }
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
features = ["default"]
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[package.metadata.playground]
|
||||
default-features = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
use super::CodegenError;
|
||||
use heck::{ToSnakeCase as _, ToUpperCamelCase as _};
|
||||
use proc_macro2::TokenStream as TokenStream2;
|
||||
use quote::{format_ident, quote};
|
||||
use scale_typegen::typegen::ir::ToTokensWithSettings;
|
||||
use scale_typegen::{TypeGenerator, typegen::ir::type_ir::CompositeIRKind};
|
||||
use pezkuwi_subxt_metadata::PalletMetadata;
|
||||
|
||||
/// Generate calls from the provided pallet's metadata. Each call returns a `StaticPayload`
|
||||
/// that can be passed to the subxt client to submit/sign/encode.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `type_gen` - [`scale_typegen::TypeGenerator`] that contains settings and all types from the runtime metadata.
|
||||
/// - `pallet` - Pallet metadata from which the calls are generated.
|
||||
/// - `crate_path` - The crate path under which the `subxt-core` crate is located, e.g. `::pezkuwi_subxt::ext::pezkuwi_subxt_core` when using subxt as a dependency.
|
||||
pub fn generate_calls(
|
||||
type_gen: &TypeGenerator,
|
||||
pallet: &PalletMetadata,
|
||||
crate_path: &syn::Path,
|
||||
) -> Result<TokenStream2, CodegenError> {
|
||||
// Early return if the pallet has no calls.
|
||||
let Some(call_ty) = pallet.call_ty_id() else {
|
||||
return Ok(quote!());
|
||||
};
|
||||
|
||||
let variant_names_and_struct_defs = super::generate_structs_from_variants(
|
||||
type_gen,
|
||||
call_ty,
|
||||
|name| name.to_upper_camel_case().into(),
|
||||
"Call",
|
||||
)?;
|
||||
let (call_structs, call_fns): (Vec<_>, Vec<_>) = variant_names_and_struct_defs
|
||||
.into_iter()
|
||||
.map(|var| {
|
||||
let (call_fn_args, call_args): (Vec<_>, Vec<_>) = match &var.composite.kind {
|
||||
CompositeIRKind::Named(named_fields) => named_fields
|
||||
.iter()
|
||||
.map(|(name, field)| {
|
||||
// Note: fn_arg_type this is relative the type path of the type alias when prefixed with `types::`, e.g. `set_max_code_size::New`
|
||||
let fn_arg_type = field.type_path.to_token_stream(type_gen.settings());
|
||||
let call_arg = if field.is_boxed {
|
||||
quote! { #name: #crate_path::alloc::boxed::Box::new(#name) }
|
||||
} else {
|
||||
quote! { #name }
|
||||
};
|
||||
(quote!( #name: types::#fn_arg_type ), call_arg)
|
||||
})
|
||||
.unzip(),
|
||||
CompositeIRKind::NoFields => Default::default(),
|
||||
CompositeIRKind::Unnamed(_) => {
|
||||
return Err(CodegenError::InvalidCallVariant(call_ty));
|
||||
}
|
||||
};
|
||||
|
||||
let pallet_name = pallet.name();
|
||||
let call_name = &var.variant_name;
|
||||
let struct_name = &var.composite.name;
|
||||
let Some(call_hash) = pallet.call_hash(call_name) else {
|
||||
return Err(CodegenError::MissingCallMetadata(
|
||||
pallet_name.into(),
|
||||
call_name.to_string(),
|
||||
));
|
||||
};
|
||||
let fn_name = format_ident!("{}", var.variant_name.to_snake_case());
|
||||
// Propagate the documentation just to `TransactionApi` methods, while
|
||||
// draining the documentation of inner call structures.
|
||||
let docs = &var.composite.docs;
|
||||
|
||||
// this converts the composite into a full struct type. No Type Parameters needed here.
|
||||
let struct_def = type_gen
|
||||
.upcast_composite(&var.composite)
|
||||
.to_token_stream(type_gen.settings());
|
||||
let alias_mod = var.type_alias_mod;
|
||||
// The call structure's documentation was stripped above.
|
||||
let call_struct = quote! {
|
||||
#struct_def
|
||||
#alias_mod
|
||||
|
||||
impl #crate_path::blocks::StaticExtrinsic for #struct_name {
|
||||
const PALLET: &'static str = #pallet_name;
|
||||
const CALL: &'static str = #call_name;
|
||||
}
|
||||
};
|
||||
|
||||
let client_fn = quote! {
|
||||
#docs
|
||||
pub fn #fn_name(
|
||||
&self,
|
||||
#( #call_fn_args, )*
|
||||
) -> #crate_path::tx::payload::StaticPayload<types::#struct_name> {
|
||||
#crate_path::tx::payload::StaticPayload::new_static(
|
||||
#pallet_name,
|
||||
#call_name,
|
||||
types::#struct_name { #( #call_args, )* },
|
||||
[#(#call_hash,)*]
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
Ok((call_struct, client_fn))
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?
|
||||
.into_iter()
|
||||
.unzip();
|
||||
|
||||
let call_type = type_gen
|
||||
.resolve_type_path(call_ty)?
|
||||
.to_token_stream(type_gen.settings());
|
||||
let call_ty = type_gen.resolve_type(call_ty)?;
|
||||
let docs = type_gen.docs_from_scale_info(&call_ty.docs);
|
||||
|
||||
let types_mod_ident = type_gen.types_mod_ident();
|
||||
|
||||
Ok(quote! {
|
||||
#docs
|
||||
pub type Call = #call_type;
|
||||
pub mod calls {
|
||||
use super::root_mod;
|
||||
use super::#types_mod_ident;
|
||||
|
||||
type DispatchError = #types_mod_ident::pezsp_runtime::DispatchError;
|
||||
|
||||
pub mod types {
|
||||
use super::#types_mod_ident;
|
||||
|
||||
#( #call_structs )*
|
||||
}
|
||||
|
||||
pub struct TransactionApi;
|
||||
|
||||
impl TransactionApi {
|
||||
#( #call_fns )*
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
use heck::ToSnakeCase as _;
|
||||
use proc_macro2::TokenStream as TokenStream2;
|
||||
use quote::{format_ident, quote};
|
||||
use scale_typegen::TypeGenerator;
|
||||
use scale_typegen::typegen::ir::ToTokensWithSettings;
|
||||
use pezkuwi_subxt_metadata::PalletMetadata;
|
||||
|
||||
use super::CodegenError;
|
||||
|
||||
/// Generate constants from the provided pallet's metadata.
|
||||
///
|
||||
/// The function creates a new module named `constants` under the pallet's module.
|
||||
/// ```rust,ignore
|
||||
/// pub mod PalletName {
|
||||
/// pub mod constants {
|
||||
/// ...
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// The constants are exposed via the `ConstantsApi` wrapper.
|
||||
///
|
||||
/// Although the constants are defined in the provided static metadata, the API
|
||||
/// ensures that the constants are returned from the runtime metadata of the node.
|
||||
/// This ensures that if the node's constants change value, we'll always see the latest values.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `type_gen` - [`scale_typegen::TypeGenerator`] that contains settings and all types from the runtime metadata.
|
||||
/// - `pallet` - Pallet metadata from which the constants are generated.
|
||||
/// - `crate_path` - The crate path under which the `subxt-core` crate is located, e.g. `::pezkuwi_subxt::ext::pezkuwi_subxt_core` when using subxt as a dependency.
|
||||
pub fn generate_constants(
|
||||
type_gen: &TypeGenerator,
|
||||
pallet: &PalletMetadata,
|
||||
crate_path: &syn::Path,
|
||||
) -> Result<TokenStream2, CodegenError> {
|
||||
// Early return if the pallet has no constants.
|
||||
if pallet.constants().len() == 0 {
|
||||
return Ok(quote!());
|
||||
}
|
||||
|
||||
let constant_fns = pallet
|
||||
.constants()
|
||||
.map(|constant| {
|
||||
let fn_name = format_ident!("{}", constant.name().to_snake_case());
|
||||
let pallet_name = pallet.name();
|
||||
let constant_name = constant.name();
|
||||
let Some(constant_hash) = pallet.constant_hash(constant_name) else {
|
||||
return Err(CodegenError::MissingConstantMetadata(
|
||||
constant_name.into(),
|
||||
pallet_name.into(),
|
||||
));
|
||||
};
|
||||
|
||||
let return_ty = type_gen
|
||||
.resolve_type_path(constant.ty())?
|
||||
.to_token_stream(type_gen.settings());
|
||||
let docs = constant.docs();
|
||||
let docs = type_gen
|
||||
.settings()
|
||||
.should_gen_docs
|
||||
.then_some(quote! { #( #[doc = #docs ] )* })
|
||||
.unwrap_or_default();
|
||||
|
||||
Ok(quote! {
|
||||
#docs
|
||||
pub fn #fn_name(&self) -> #crate_path::constants::address::StaticAddress<#return_ty> {
|
||||
#crate_path::constants::address::StaticAddress::new_static(
|
||||
#pallet_name,
|
||||
#constant_name,
|
||||
[#(#constant_hash,)*]
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
let types_mod_ident = type_gen.types_mod_ident();
|
||||
|
||||
Ok(quote! {
|
||||
pub mod constants {
|
||||
use super::#types_mod_ident;
|
||||
|
||||
pub struct ConstantsApi;
|
||||
|
||||
impl ConstantsApi {
|
||||
#(#constant_fns)*
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
use heck::ToSnakeCase as _;
|
||||
use scale_typegen::TypeGenerator;
|
||||
use scale_typegen::typegen::ir::ToTokensWithSettings;
|
||||
use std::collections::HashSet;
|
||||
use pezkuwi_subxt_metadata::{CustomValueMetadata, Metadata};
|
||||
|
||||
use proc_macro2::TokenStream as TokenStream2;
|
||||
use quote::quote;
|
||||
|
||||
/// Generate the custom values mod, if there are any custom values in the metadata. Else returns None.
|
||||
pub fn generate_custom_values(
|
||||
metadata: &Metadata,
|
||||
type_gen: &TypeGenerator,
|
||||
crate_path: &syn::Path,
|
||||
) -> TokenStream2 {
|
||||
let mut fn_names_taken = HashSet::new();
|
||||
let custom = metadata.custom();
|
||||
let custom_values_fns = custom.iter().filter_map(|custom_value| {
|
||||
generate_custom_value_fn(custom_value, type_gen, crate_path, &mut fn_names_taken)
|
||||
});
|
||||
|
||||
quote! {
|
||||
pub struct CustomValuesApi;
|
||||
|
||||
impl CustomValuesApi {
|
||||
#(#custom_values_fns)*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates runtime functions for the given API metadata.
|
||||
/// Returns None, if the name would not make for a valid identifier.
|
||||
fn generate_custom_value_fn(
|
||||
custom_value: CustomValueMetadata,
|
||||
type_gen: &TypeGenerator,
|
||||
crate_path: &syn::Path,
|
||||
fn_names_taken: &mut HashSet<String>,
|
||||
) -> Option<TokenStream2> {
|
||||
// names are transformed to snake case to make for good function identifiers.
|
||||
let name = custom_value.name();
|
||||
let fn_name = name.to_snake_case();
|
||||
if fn_names_taken.contains(&fn_name) {
|
||||
return None;
|
||||
}
|
||||
// if the fn_name would be an invalid ident, return None:
|
||||
let fn_name_ident = syn::parse_str::<syn::Ident>(&fn_name).ok()?;
|
||||
fn_names_taken.insert(fn_name);
|
||||
|
||||
let custom_value_hash = custom_value.hash();
|
||||
|
||||
// for custom values it is important to check if the type id is actually in the metadata:
|
||||
let type_is_valid = custom_value
|
||||
.types()
|
||||
.resolve(custom_value.type_id())
|
||||
.is_some();
|
||||
|
||||
let (return_ty, decodable) = if type_is_valid {
|
||||
let return_ty = type_gen
|
||||
.resolve_type_path(custom_value.type_id())
|
||||
.expect("type is in metadata; qed")
|
||||
.to_token_stream(type_gen.settings());
|
||||
let decodable = quote!(#crate_path::utils::Maybe);
|
||||
(return_ty, decodable)
|
||||
} else {
|
||||
// if type registry does not contain the type, we can just return the Encoded scale bytes.
|
||||
(quote!(()), quote!(#crate_path::utils::No))
|
||||
};
|
||||
|
||||
Some(quote!(
|
||||
pub fn #fn_name_ident(&self) -> #crate_path::custom_values::address::StaticAddress<#return_ty, #decodable> {
|
||||
#crate_path::custom_values::address::StaticAddress::new_static(#name, [#(#custom_value_hash,)*])
|
||||
}
|
||||
))
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
use proc_macro2::TokenStream as TokenStream2;
|
||||
use quote::quote;
|
||||
use scale_typegen::TypeGenerator;
|
||||
use pezkuwi_subxt_metadata::PalletMetadata;
|
||||
|
||||
use super::CodegenError;
|
||||
use scale_typegen::typegen::ir::ToTokensWithSettings;
|
||||
|
||||
/// Generate error type alias from the provided pallet metadata.
|
||||
pub fn generate_error_type_alias(
|
||||
type_gen: &TypeGenerator,
|
||||
pallet: &PalletMetadata,
|
||||
) -> Result<TokenStream2, CodegenError> {
|
||||
let Some(error_ty) = pallet.error_ty_id() else {
|
||||
return Ok(quote!());
|
||||
};
|
||||
|
||||
let error_type = type_gen
|
||||
.resolve_type_path(error_ty)?
|
||||
.to_token_stream(type_gen.settings());
|
||||
let error_ty = type_gen.resolve_type(error_ty)?;
|
||||
let docs = &error_ty.docs;
|
||||
let docs = type_gen
|
||||
.settings()
|
||||
.should_gen_docs
|
||||
.then_some(quote! { #( #[doc = #docs ] )* })
|
||||
.unwrap_or_default();
|
||||
Ok(quote! {
|
||||
#docs
|
||||
pub type Error = #error_type;
|
||||
})
|
||||
}
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
use super::CodegenError;
|
||||
use proc_macro2::TokenStream as TokenStream2;
|
||||
use quote::quote;
|
||||
use scale_typegen::TypeGenerator;
|
||||
use scale_typegen::typegen::ir::ToTokensWithSettings;
|
||||
use pezkuwi_subxt_metadata::PalletMetadata;
|
||||
|
||||
/// Generate events from the provided pallet metadata.
|
||||
///
|
||||
/// The function creates a new module named `events` under the pallet's module.
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// pub mod PalletName {
|
||||
/// pub mod events {
|
||||
/// ...
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// The function generates the events as rust structs that implement the `subxt::event::StaticEvent` trait
|
||||
/// to uniquely identify the event's identity when creating the extrinsic.
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// pub struct EventName {
|
||||
/// pub event_param: type,
|
||||
/// }
|
||||
/// impl ::pezkuwi_subxt::events::StaticEvent for EventName {
|
||||
/// ...
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `type_gen` - [`scale_typegen::TypeGenerator`] that contains settings and all types from the runtime metadata.
|
||||
/// - `pallet` - Pallet metadata from which the events are generated.
|
||||
/// - `crate_path` - The crate path under which the `subxt-core` crate is located, e.g. `::pezkuwi_subxt::ext::pezkuwi_subxt_core` when using subxt as a dependency.
|
||||
pub fn generate_events(
|
||||
type_gen: &TypeGenerator,
|
||||
pallet: &PalletMetadata,
|
||||
crate_path: &syn::Path,
|
||||
) -> Result<TokenStream2, CodegenError> {
|
||||
// Early return if the pallet has no events.
|
||||
let Some(event_ty) = pallet.event_ty_id() else {
|
||||
return Ok(quote!());
|
||||
};
|
||||
|
||||
let variant_names_and_struct_defs =
|
||||
super::generate_structs_from_variants(type_gen, event_ty, |name| name.into(), "Event")?;
|
||||
|
||||
let event_structs = variant_names_and_struct_defs.into_iter().map(|var| {
|
||||
let pallet_name = pallet.name();
|
||||
let event_struct_name = &var.composite.name;
|
||||
let event_name = var.variant_name;
|
||||
let alias_mod = var.type_alias_mod;
|
||||
let struct_def = type_gen
|
||||
.upcast_composite(&var.composite)
|
||||
.to_token_stream(type_gen.settings());
|
||||
quote! {
|
||||
#struct_def
|
||||
#alias_mod
|
||||
|
||||
impl #crate_path::events::StaticEvent for #event_struct_name {
|
||||
const PALLET: &'static str = #pallet_name;
|
||||
const EVENT: &'static str = #event_name;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let event_type = type_gen
|
||||
.resolve_type_path(event_ty)?
|
||||
.to_token_stream(type_gen.settings());
|
||||
let event_ty = type_gen.resolve_type(event_ty)?;
|
||||
let docs = &event_ty.docs;
|
||||
let docs = type_gen
|
||||
.settings()
|
||||
.should_gen_docs
|
||||
.then_some(quote! { #( #[doc = #docs ] )* })
|
||||
.unwrap_or_default();
|
||||
let types_mod_ident = type_gen.types_mod_ident();
|
||||
|
||||
Ok(quote! {
|
||||
#docs
|
||||
pub type Event = #event_type;
|
||||
pub mod events {
|
||||
use super::#types_mod_ident;
|
||||
#( #event_structs )*
|
||||
}
|
||||
})
|
||||
}
|
||||
+475
@@ -0,0 +1,475 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! Generate code for submitting extrinsics and query storage of a Substrate runtime.
|
||||
|
||||
mod calls;
|
||||
mod constants;
|
||||
mod custom_values;
|
||||
mod errors;
|
||||
mod events;
|
||||
mod pallet_view_functions;
|
||||
mod runtime_apis;
|
||||
mod storage;
|
||||
|
||||
use scale_typegen::TypeGenerator;
|
||||
use scale_typegen::typegen::ir::ToTokensWithSettings;
|
||||
use scale_typegen::typegen::ir::type_ir::{CompositeFieldIR, CompositeIR, CompositeIRKind};
|
||||
use scale_typegen::typegen::type_params::TypeParameters;
|
||||
use scale_typegen::typegen::type_path::TypePath;
|
||||
use pezkuwi_subxt_metadata::Metadata;
|
||||
use syn::{Ident, parse_quote};
|
||||
|
||||
use crate::error::CodegenError;
|
||||
use crate::subxt_type_gen_settings;
|
||||
use crate::{api::custom_values::generate_custom_values, ir};
|
||||
|
||||
use heck::{ToSnakeCase as _, ToUpperCamelCase};
|
||||
use proc_macro2::TokenStream as TokenStream2;
|
||||
use quote::{format_ident, quote};
|
||||
|
||||
/// Create the API for interacting with a Substrate runtime.
|
||||
pub struct RuntimeGenerator {
|
||||
metadata: Metadata,
|
||||
}
|
||||
|
||||
impl RuntimeGenerator {
|
||||
/// Create a new runtime generator from the provided metadata.
|
||||
///
|
||||
/// **Note:** If you have the metadata path, URL or bytes to hand, prefer to use
|
||||
/// `GenerateRuntimeApi` for generating the runtime API from that.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if the runtime metadata version is not supported.
|
||||
///
|
||||
/// Supported versions: v14 and v15.
|
||||
pub fn new(mut metadata: Metadata) -> Self {
|
||||
scale_typegen::utils::ensure_unique_type_paths(metadata.types_mut())
|
||||
.expect("Duplicate type paths in metadata; this is bug please file an issue.");
|
||||
RuntimeGenerator { metadata }
|
||||
}
|
||||
|
||||
/// Generate the API for interacting with a Substrate runtime.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `item_mod` - The module declaration for which the API is implemented.
|
||||
/// * `derives` - Provide custom derives for the generated types.
|
||||
/// * `type_substitutes` - Provide custom type substitutes.
|
||||
/// * `crate_path` - Path to the `subxt` crate.
|
||||
/// * `should_gen_docs` - True if the generated API contains the documentation from the metadata.
|
||||
pub fn generate_runtime_types(
|
||||
&self,
|
||||
item_mod: syn::ItemMod,
|
||||
derives: scale_typegen::DerivesRegistry,
|
||||
type_substitutes: scale_typegen::TypeSubstitutes,
|
||||
crate_path: syn::Path,
|
||||
should_gen_docs: bool,
|
||||
) -> Result<TokenStream2, CodegenError> {
|
||||
let item_mod_attrs = item_mod.attrs.clone();
|
||||
let item_mod_ir = ir::ItemMod::try_from(item_mod)?;
|
||||
|
||||
let settings =
|
||||
subxt_type_gen_settings(derives, type_substitutes, &crate_path, should_gen_docs);
|
||||
|
||||
let type_gen = TypeGenerator::new(self.metadata.types(), &settings);
|
||||
let types_mod = type_gen
|
||||
.generate_types_mod()?
|
||||
.to_token_stream(type_gen.settings());
|
||||
let mod_ident = &item_mod_ir.ident;
|
||||
let rust_items = item_mod_ir.rust_items();
|
||||
|
||||
Ok(quote! {
|
||||
#( #item_mod_attrs )*
|
||||
#[allow(dead_code, unused_imports, non_camel_case_types, unreachable_patterns)]
|
||||
#[allow(clippy::all)]
|
||||
#[allow(rustdoc::broken_intra_doc_links)]
|
||||
pub mod #mod_ident {
|
||||
// Preserve any Rust items that were previously defined in the adorned module
|
||||
#( #rust_items ) *
|
||||
|
||||
// Make it easy to access the root items via `root_mod` at different levels
|
||||
// without reaching out of this module.
|
||||
#[allow(unused_imports)]
|
||||
mod root_mod {
|
||||
pub use super::*;
|
||||
}
|
||||
|
||||
#types_mod
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Generate the API for interacting with a Substrate runtime.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `item_mod` - The module declaration for which the API is implemented.
|
||||
/// * `derives` - Provide custom derives for the generated types.
|
||||
/// * `type_substitutes` - Provide custom type substitutes.
|
||||
/// * `crate_path` - Path to the `subxt` crate.
|
||||
/// * `should_gen_docs` - True if the generated API contains the documentation from the metadata.
|
||||
pub fn generate_runtime(
|
||||
&self,
|
||||
item_mod: syn::ItemMod,
|
||||
derives: scale_typegen::DerivesRegistry,
|
||||
type_substitutes: scale_typegen::TypeSubstitutes,
|
||||
crate_path: syn::Path,
|
||||
should_gen_docs: bool,
|
||||
) -> Result<TokenStream2, CodegenError> {
|
||||
let item_mod_attrs = item_mod.attrs.clone();
|
||||
let item_mod_ir = ir::ItemMod::try_from(item_mod)?;
|
||||
|
||||
let settings =
|
||||
subxt_type_gen_settings(derives, type_substitutes, &crate_path, should_gen_docs);
|
||||
|
||||
let type_gen = TypeGenerator::new(self.metadata.types(), &settings);
|
||||
let types_mod = type_gen
|
||||
.generate_types_mod()?
|
||||
.to_token_stream(type_gen.settings());
|
||||
let types_mod_ident = type_gen.types_mod_ident();
|
||||
let pallets_with_mod_names = self
|
||||
.metadata
|
||||
.pallets()
|
||||
.map(|pallet| {
|
||||
(
|
||||
pallet,
|
||||
format_ident!("{}", pallet.name().to_string().to_snake_case()),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Pallet names and their length are used to create PALLETS array.
|
||||
// The array is used to identify the pallets composing the metadata for
|
||||
// validation of just those pallets.
|
||||
let pallet_names: Vec<_> = self
|
||||
.metadata
|
||||
.pallets()
|
||||
.map(|pallet| pallet.name())
|
||||
.collect();
|
||||
let pallet_names_len = pallet_names.len();
|
||||
|
||||
let runtime_api_names: Vec<_> = self
|
||||
.metadata
|
||||
.runtime_api_traits()
|
||||
.map(|api| api.name().to_string())
|
||||
.collect();
|
||||
let runtime_api_names_len = runtime_api_names.len();
|
||||
|
||||
let modules = pallets_with_mod_names
|
||||
.iter()
|
||||
.map(|(pallet, mod_name)| {
|
||||
let calls = calls::generate_calls(&type_gen, pallet, &crate_path)?;
|
||||
|
||||
let event = events::generate_events(&type_gen, pallet, &crate_path)?;
|
||||
|
||||
let storage_mod = storage::generate_storage(&type_gen, pallet, &crate_path)?;
|
||||
|
||||
let constants_mod = constants::generate_constants(&type_gen, pallet, &crate_path)?;
|
||||
|
||||
let errors = errors::generate_error_type_alias(&type_gen, pallet)?;
|
||||
|
||||
let view_functions = pallet_view_functions::generate_pallet_view_functions(
|
||||
&type_gen,
|
||||
pallet,
|
||||
&crate_path,
|
||||
)?;
|
||||
|
||||
Ok(quote! {
|
||||
pub mod #mod_name {
|
||||
use super::root_mod;
|
||||
use super::#types_mod_ident;
|
||||
#errors
|
||||
#calls
|
||||
#view_functions
|
||||
#event
|
||||
#storage_mod
|
||||
#constants_mod
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>, CodegenError>>()?;
|
||||
|
||||
let mod_ident = &item_mod_ir.ident;
|
||||
let pallets_with_constants: Vec<_> = pallets_with_mod_names
|
||||
.iter()
|
||||
.filter_map(|(pallet, pallet_mod_name)| {
|
||||
pallet
|
||||
.constants()
|
||||
.next()
|
||||
.is_some()
|
||||
.then_some(pallet_mod_name)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let pallets_with_storage: Vec<_> = pallets_with_mod_names
|
||||
.iter()
|
||||
.filter_map(|(pallet, pallet_mod_name)| pallet.storage().map(|_| pallet_mod_name))
|
||||
.collect();
|
||||
|
||||
let pallets_with_calls: Vec<_> = pallets_with_mod_names
|
||||
.iter()
|
||||
.filter_map(|(pallet, pallet_mod_name)| pallet.call_ty_id().map(|_| pallet_mod_name))
|
||||
.collect();
|
||||
|
||||
let pallets_with_view_functions: Vec<_> = pallets_with_mod_names
|
||||
.iter()
|
||||
.filter(|(pallet, _pallet_mod_name)| pallet.has_view_functions())
|
||||
.map(|(_, pallet_mod_name)| pallet_mod_name)
|
||||
.collect();
|
||||
|
||||
let rust_items = item_mod_ir.rust_items();
|
||||
|
||||
let apis_mod = runtime_apis::generate_runtime_apis(
|
||||
&self.metadata,
|
||||
&type_gen,
|
||||
types_mod_ident,
|
||||
&crate_path,
|
||||
)?;
|
||||
|
||||
// Fetch the paths of the outer enums.
|
||||
// Substrate exposes those under `kitchensink_runtime`, while Polkadot under `polkadot_runtime`.
|
||||
let call_path = type_gen
|
||||
.resolve_type_path(self.metadata.outer_enums().call_enum_ty())?
|
||||
.to_token_stream(type_gen.settings());
|
||||
let event_path = type_gen
|
||||
.resolve_type_path(self.metadata.outer_enums().event_enum_ty())?
|
||||
.to_token_stream(type_gen.settings());
|
||||
let error_path = type_gen
|
||||
.resolve_type_path(self.metadata.outer_enums().error_enum_ty())?
|
||||
.to_token_stream(type_gen.settings());
|
||||
|
||||
let metadata_hash = self.metadata.hasher().hash();
|
||||
|
||||
let custom_values = generate_custom_values(&self.metadata, &type_gen, &crate_path);
|
||||
|
||||
Ok(quote! {
|
||||
#( #item_mod_attrs )*
|
||||
#[allow(dead_code, unused_imports, non_camel_case_types, unreachable_patterns)]
|
||||
#[allow(clippy::all)]
|
||||
#[allow(rustdoc::broken_intra_doc_links)]
|
||||
pub mod #mod_ident {
|
||||
// Preserve any Rust items that were previously defined in the adorned module.
|
||||
#( #rust_items ) *
|
||||
|
||||
// Make it easy to access the root items via `root_mod` at different levels
|
||||
// without reaching out of this module.
|
||||
#[allow(unused_imports)]
|
||||
mod root_mod {
|
||||
pub use super::*;
|
||||
}
|
||||
|
||||
// Identify the pallets composing the static metadata by name.
|
||||
pub static PALLETS: [&str; #pallet_names_len] = [ #(#pallet_names,)* ];
|
||||
|
||||
// Runtime APIs in the metadata by name.
|
||||
pub static RUNTIME_APIS: [&str; #runtime_api_names_len] = [ #(#runtime_api_names,)* ];
|
||||
|
||||
/// The error type that is returned when there is a runtime issue.
|
||||
pub type DispatchError = #types_mod_ident::pezsp_runtime::DispatchError;
|
||||
|
||||
/// The outer event enum.
|
||||
pub type Event = #event_path;
|
||||
|
||||
/// The outer extrinsic enum.
|
||||
pub type Call = #call_path;
|
||||
|
||||
/// The outer error enum represents the DispatchError's Module variant.
|
||||
pub type Error = #error_path;
|
||||
|
||||
pub fn constants() -> ConstantsApi {
|
||||
ConstantsApi
|
||||
}
|
||||
|
||||
pub fn storage() -> StorageApi {
|
||||
StorageApi
|
||||
}
|
||||
|
||||
pub fn tx() -> TransactionApi {
|
||||
TransactionApi
|
||||
}
|
||||
|
||||
pub fn apis() -> runtime_apis::RuntimeApi {
|
||||
runtime_apis::RuntimeApi
|
||||
}
|
||||
|
||||
#apis_mod
|
||||
|
||||
pub fn view_functions() -> ViewFunctionsApi {
|
||||
ViewFunctionsApi
|
||||
}
|
||||
|
||||
pub fn custom() -> CustomValuesApi {
|
||||
CustomValuesApi
|
||||
}
|
||||
|
||||
#custom_values
|
||||
|
||||
pub struct ConstantsApi;
|
||||
impl ConstantsApi {
|
||||
#(
|
||||
pub fn #pallets_with_constants(&self) -> #pallets_with_constants::constants::ConstantsApi {
|
||||
#pallets_with_constants::constants::ConstantsApi
|
||||
}
|
||||
)*
|
||||
}
|
||||
|
||||
pub struct StorageApi;
|
||||
impl StorageApi {
|
||||
#(
|
||||
pub fn #pallets_with_storage(&self) -> #pallets_with_storage::storage::StorageApi {
|
||||
#pallets_with_storage::storage::StorageApi
|
||||
}
|
||||
)*
|
||||
}
|
||||
|
||||
pub struct TransactionApi;
|
||||
impl TransactionApi {
|
||||
#(
|
||||
pub fn #pallets_with_calls(&self) -> #pallets_with_calls::calls::TransactionApi {
|
||||
#pallets_with_calls::calls::TransactionApi
|
||||
}
|
||||
)*
|
||||
}
|
||||
|
||||
pub struct ViewFunctionsApi;
|
||||
impl ViewFunctionsApi {
|
||||
#(
|
||||
pub fn #pallets_with_view_functions(&self) -> #pallets_with_view_functions::view_functions::ViewFunctionsApi {
|
||||
#pallets_with_view_functions::view_functions::ViewFunctionsApi
|
||||
}
|
||||
)*
|
||||
}
|
||||
|
||||
/// check whether the metadata provided is aligned with this statically generated code.
|
||||
pub fn is_codegen_valid_for(metadata: &#crate_path::Metadata) -> bool {
|
||||
let runtime_metadata_hash = metadata
|
||||
.hasher()
|
||||
.only_these_pallets(&PALLETS)
|
||||
.only_these_runtime_apis(&RUNTIME_APIS)
|
||||
.hash();
|
||||
runtime_metadata_hash == [ #(#metadata_hash,)* ]
|
||||
}
|
||||
|
||||
#( #modules )*
|
||||
#types_mod
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a vector of tuples of variant names and corresponding struct definitions.
|
||||
pub fn generate_structs_from_variants<F>(
|
||||
type_gen: &TypeGenerator,
|
||||
type_id: u32,
|
||||
variant_to_struct_name: F,
|
||||
error_message_type_name: &str,
|
||||
) -> Result<Vec<StructFromVariant>, CodegenError>
|
||||
where
|
||||
F: Fn(&str) -> std::borrow::Cow<str>,
|
||||
{
|
||||
let ty = type_gen.resolve_type(type_id)?;
|
||||
|
||||
let scale_info::TypeDef::Variant(variant) = &ty.type_def else {
|
||||
return Err(CodegenError::InvalidType(error_message_type_name.into()));
|
||||
};
|
||||
|
||||
variant
|
||||
.variants
|
||||
.iter()
|
||||
.map(|var| {
|
||||
let mut type_params = TypeParameters::from_scale_info(&[]);
|
||||
let composite_ir_kind =
|
||||
type_gen.create_composite_ir_kind(&var.fields, &mut type_params)?;
|
||||
let struct_name = variant_to_struct_name(&var.name);
|
||||
let mut composite = CompositeIR::new(
|
||||
syn::parse_str(&struct_name).expect("enum variant is a valid ident; qed"),
|
||||
composite_ir_kind,
|
||||
type_gen.docs_from_scale_info(&var.docs),
|
||||
);
|
||||
|
||||
let type_alias_mod = generate_type_alias_mod(&mut composite, type_gen);
|
||||
Ok(StructFromVariant {
|
||||
variant_name: var.name.to_string(),
|
||||
composite,
|
||||
type_alias_mod,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub struct StructFromVariant {
|
||||
variant_name: String,
|
||||
composite: CompositeIR,
|
||||
type_alias_mod: TokenStream2,
|
||||
}
|
||||
|
||||
/// Modifies the composite, by replacing its types with references to the generated type alias module.
|
||||
/// Returns the TokenStream of the type alias module.
|
||||
///
|
||||
/// E.g a struct like this:
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// pub struct SetMaxCodeSize {
|
||||
/// pub new: ::core::primitive::u32,
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// will be made into this:
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// pub struct SetMaxCodeSize {
|
||||
/// pub new: set_max_code_size::New,
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// And the type alias module will look like this:
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// pub mod set_max_code_size {
|
||||
/// use super::runtime_types;
|
||||
/// pub type New = ::core::primitive::u32;
|
||||
/// }
|
||||
/// ```
|
||||
pub fn generate_type_alias_mod(
|
||||
composite: &mut CompositeIR,
|
||||
type_gen: &TypeGenerator,
|
||||
) -> TokenStream2 {
|
||||
let mut aliases: Vec<TokenStream2> = vec![];
|
||||
let alias_mod_name: Ident = syn::parse_str(&composite.name.to_string().to_snake_case())
|
||||
.expect("composite name in snake_case should be a valid identifier");
|
||||
|
||||
let mut modify_field_to_be_type_alias = |field: &mut CompositeFieldIR, alias_name: Ident| {
|
||||
let type_path = field.type_path.to_token_stream(type_gen.settings());
|
||||
aliases.push(quote!(pub type #alias_name = #type_path;));
|
||||
|
||||
let type_alias_path: syn::Path = parse_quote!(#alias_mod_name::#alias_name);
|
||||
field.type_path = TypePath::from_syn_path(type_alias_path);
|
||||
};
|
||||
|
||||
match &mut composite.kind {
|
||||
CompositeIRKind::NoFields => {
|
||||
return quote!(); // no types mod generated for unit structs.
|
||||
}
|
||||
CompositeIRKind::Named(named) => {
|
||||
for (name, field) in named.iter_mut() {
|
||||
let alias_name = format_ident!("{}", name.to_string().to_upper_camel_case());
|
||||
modify_field_to_be_type_alias(field, alias_name);
|
||||
}
|
||||
}
|
||||
CompositeIRKind::Unnamed(unnamed) => {
|
||||
for (i, field) in unnamed.iter_mut().enumerate() {
|
||||
let alias_name = format_ident!("Field{}", i);
|
||||
modify_field_to_be_type_alias(field, alias_name);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let types_mod_ident = type_gen.types_mod_ident();
|
||||
quote!(pub mod #alias_mod_name {
|
||||
use super::#types_mod_ident;
|
||||
#( #aliases )*
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
use heck::ToUpperCamelCase as _;
|
||||
|
||||
use crate::CodegenError;
|
||||
use proc_macro2::TokenStream as TokenStream2;
|
||||
use quote::{format_ident, quote};
|
||||
use scale_typegen::TypeGenerator;
|
||||
use scale_typegen::typegen::ir::ToTokensWithSettings;
|
||||
use std::collections::HashSet;
|
||||
use pezkuwi_subxt_metadata::{PalletMetadata, ViewFunctionMetadata};
|
||||
|
||||
pub fn generate_pallet_view_functions(
|
||||
type_gen: &TypeGenerator,
|
||||
pallet: &PalletMetadata,
|
||||
crate_path: &syn::Path,
|
||||
) -> Result<TokenStream2, CodegenError> {
|
||||
if !pallet.has_view_functions() {
|
||||
// If there are no view functions in this pallet, we
|
||||
// don't generate anything.
|
||||
return Ok(quote! {});
|
||||
}
|
||||
|
||||
let view_functions: Vec<_> = pallet
|
||||
.view_functions()
|
||||
.map(|vf| generate_pallet_view_function(pallet.name(), vf, type_gen, crate_path))
|
||||
.collect::<Result<_, _>>()?;
|
||||
|
||||
let view_functions_types = view_functions.iter().map(|(apis, _)| apis);
|
||||
let view_functions_methods = view_functions.iter().map(|(_, getters)| getters);
|
||||
|
||||
let types_mod_ident = type_gen.types_mod_ident();
|
||||
|
||||
Ok(quote! {
|
||||
pub mod view_functions {
|
||||
use super::root_mod;
|
||||
use super::#types_mod_ident;
|
||||
|
||||
pub struct ViewFunctionsApi;
|
||||
|
||||
impl ViewFunctionsApi {
|
||||
#( #view_functions_methods )*
|
||||
}
|
||||
|
||||
#( #view_functions_types )*
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_pallet_view_function(
|
||||
pallet_name: &str,
|
||||
view_function: ViewFunctionMetadata<'_>,
|
||||
type_gen: &TypeGenerator,
|
||||
crate_path: &syn::Path,
|
||||
) -> Result<(TokenStream2, TokenStream2), CodegenError> {
|
||||
let types_mod_ident = type_gen.types_mod_ident();
|
||||
|
||||
let view_function_name_str = view_function.name();
|
||||
let view_function_name_ident = format_ident!("{view_function_name_str}");
|
||||
let validation_hash = view_function.hash();
|
||||
|
||||
let docs = view_function.docs();
|
||||
let docs: TokenStream2 = type_gen
|
||||
.settings()
|
||||
.should_gen_docs
|
||||
.then_some(quote! { #( #[doc = #docs ] )* })
|
||||
.unwrap_or_default();
|
||||
|
||||
struct Input {
|
||||
name: syn::Ident,
|
||||
type_alias: syn::Ident,
|
||||
type_path: TokenStream2,
|
||||
}
|
||||
|
||||
let view_function_inputs: Vec<Input> = {
|
||||
let mut unique_names = HashSet::new();
|
||||
let mut unique_aliases = HashSet::new();
|
||||
|
||||
view_function
|
||||
.inputs()
|
||||
.enumerate()
|
||||
.map(|(idx, input)| {
|
||||
// These are method names, which can just be '_', but struct field names can't
|
||||
// just be an underscore, so fix any such names we find to work in structs.
|
||||
let mut name = input.name.trim_start_matches('_').to_string();
|
||||
if name.is_empty() {
|
||||
name = format!("_{idx}");
|
||||
}
|
||||
while !unique_names.insert(name.clone()) {
|
||||
name = format!("{name}_param{idx}");
|
||||
}
|
||||
|
||||
// The alias type name is based on the name, above.
|
||||
let mut alias = name.to_upper_camel_case();
|
||||
// Note: name is not empty.
|
||||
if alias.as_bytes()[0].is_ascii_digit() {
|
||||
alias = format!("Param{alias}");
|
||||
}
|
||||
while !unique_aliases.insert(alias.clone()) {
|
||||
alias = format!("{alias}Param{idx}");
|
||||
}
|
||||
|
||||
// Path to the actual type we'll have generated for this input.
|
||||
let type_path = type_gen
|
||||
.resolve_type_path(input.id)
|
||||
.expect("view function input type is in metadata; qed")
|
||||
.to_token_stream(type_gen.settings());
|
||||
|
||||
Input {
|
||||
name: format_ident!("{name}"),
|
||||
type_alias: format_ident!("{alias}"),
|
||||
type_path,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
|
||||
let input_tuple_types = view_function_inputs
|
||||
.iter()
|
||||
.map(|i| {
|
||||
let ty = &i.type_alias;
|
||||
quote!(#view_function_name_ident::#ty)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let input_args = view_function_inputs
|
||||
.iter()
|
||||
.map(|i| {
|
||||
let arg = &i.name;
|
||||
let ty = &i.type_alias;
|
||||
quote!(#arg: #view_function_name_ident::#ty)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let input_type_aliases = view_function_inputs.iter().map(|i| {
|
||||
let ty = &i.type_alias;
|
||||
let path = &i.type_path;
|
||||
quote!(pub type #ty = #path;)
|
||||
});
|
||||
|
||||
let input_param_names = view_function_inputs.iter().map(|i| &i.name);
|
||||
|
||||
let output_type_path = type_gen
|
||||
.resolve_type_path(view_function.output_ty())?
|
||||
.to_token_stream(type_gen.settings());
|
||||
|
||||
// Define the input and output type bits.
|
||||
let view_function_types = quote!(
|
||||
pub mod #view_function_name_ident {
|
||||
use super::root_mod;
|
||||
use super::#types_mod_ident;
|
||||
|
||||
#(#input_type_aliases)*
|
||||
|
||||
pub mod output {
|
||||
use super::#types_mod_ident;
|
||||
pub type Output = #output_type_path;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Define the getter method that will live on the `ViewFunctionApi` type.
|
||||
let view_function_method = quote!(
|
||||
#docs
|
||||
pub fn #view_function_name_ident(
|
||||
&self,
|
||||
#(#input_args),*
|
||||
) -> #crate_path::view_functions::payload::StaticPayload<
|
||||
(#(#input_tuple_types,)*),
|
||||
#view_function_name_ident::output::Output
|
||||
> {
|
||||
#crate_path::view_functions::payload::StaticPayload::new_static(
|
||||
#pallet_name,
|
||||
#view_function_name_str,
|
||||
(#(#input_param_names,)*),
|
||||
[#(#validation_hash,)*],
|
||||
)
|
||||
}
|
||||
);
|
||||
|
||||
Ok((view_function_types, view_function_method))
|
||||
}
|
||||
@@ -0,0 +1,433 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
use heck::ToSnakeCase as _;
|
||||
use heck::ToUpperCamelCase as _;
|
||||
|
||||
use scale_typegen::TypeGenerator;
|
||||
use scale_typegen::typegen::ir::ToTokensWithSettings;
|
||||
use pezkuwi_subxt_metadata::{Metadata, RuntimeApiMetadata};
|
||||
|
||||
use proc_macro2::TokenStream as TokenStream2;
|
||||
use quote::{format_ident, quote};
|
||||
|
||||
use crate::CodegenError;
|
||||
|
||||
/// Generate the runtime APIs.
|
||||
pub fn generate_runtime_apis(
|
||||
metadata: &Metadata,
|
||||
type_gen: &TypeGenerator,
|
||||
types_mod_ident: &syn::Ident,
|
||||
crate_path: &syn::Path,
|
||||
) -> Result<TokenStream2, CodegenError> {
|
||||
let runtime_fns: Vec<_> = metadata
|
||||
.runtime_api_traits()
|
||||
.map(|api| generate_runtime_api(api, type_gen, crate_path))
|
||||
.collect::<Result<_, _>>()?;
|
||||
|
||||
let trait_defs = runtime_fns.iter().map(|(apis, _)| apis);
|
||||
let trait_getters = runtime_fns.iter().map(|(_, getters)| getters);
|
||||
|
||||
Ok(quote! {
|
||||
pub mod runtime_apis {
|
||||
use super::root_mod;
|
||||
use super::#types_mod_ident;
|
||||
|
||||
use #crate_path::ext::codec::Encode;
|
||||
|
||||
pub struct RuntimeApi;
|
||||
|
||||
impl RuntimeApi {
|
||||
#( #trait_getters )*
|
||||
}
|
||||
|
||||
#( #trait_defs )*
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Generates runtime functions for the given API metadata.
|
||||
fn generate_runtime_api(
|
||||
api: RuntimeApiMetadata,
|
||||
type_gen: &TypeGenerator,
|
||||
crate_path: &syn::Path,
|
||||
) -> Result<(TokenStream2, TokenStream2), CodegenError> {
|
||||
let types_mod_ident = type_gen.types_mod_ident();
|
||||
// Trait name must remain as is (upper case) to identify the runtime call.
|
||||
let trait_name_str = api.name();
|
||||
// The snake case for the trait name.
|
||||
let trait_name_snake = format_ident!("{}", api.name().to_snake_case());
|
||||
|
||||
let docs = api.docs();
|
||||
let docs: TokenStream2 = type_gen
|
||||
.settings()
|
||||
.should_gen_docs
|
||||
.then_some(quote! { #( #[doc = #docs ] )* })
|
||||
.unwrap_or_default();
|
||||
|
||||
let types_and_methods = api
|
||||
.methods()
|
||||
.map(|method| {
|
||||
let method_name = format_ident!("{}", method.name());
|
||||
let method_name_str = method.name();
|
||||
let validation_hash = method.hash();
|
||||
|
||||
let docs = method.docs();
|
||||
let docs: TokenStream2 = type_gen
|
||||
.settings()
|
||||
.should_gen_docs
|
||||
.then_some(quote! { #( #[doc = #docs ] )* })
|
||||
.unwrap_or_default();
|
||||
|
||||
struct Input {
|
||||
name: syn::Ident,
|
||||
type_alias: syn::Ident,
|
||||
type_path: TokenStream2,
|
||||
}
|
||||
|
||||
let runtime_api_inputs: Vec<Input> = {
|
||||
let mut unique_names = HashSet::new();
|
||||
let mut unique_aliases = HashSet::new();
|
||||
|
||||
method
|
||||
.inputs()
|
||||
.enumerate()
|
||||
.map(|(idx, input)| {
|
||||
// The method argument name is either the input name or the
|
||||
// index (eg _1, _2 etc) if one isn't provided.
|
||||
// if we get unlucky we'll end up with param_param1 etc.
|
||||
let mut name = input.name.trim_start_matches('_').to_string();
|
||||
if name.is_empty() {
|
||||
name = format!("_{idx}");
|
||||
}
|
||||
while !unique_names.insert(name.clone()) {
|
||||
name = format!("{name}_param{idx}");
|
||||
}
|
||||
|
||||
// The alias is either InputName if provided, or Param1, Param2 etc if not.
|
||||
// If we get unlucky we may even end up with ParamParam1 etc.
|
||||
let mut alias = name.trim_start_matches('_').to_upper_camel_case();
|
||||
// Note: name is not empty.
|
||||
if alias.as_bytes()[0].is_ascii_digit() {
|
||||
alias = format!("Param{alias}");
|
||||
}
|
||||
while !unique_aliases.insert(alias.clone()) {
|
||||
alias = format!("{alias}Param{idx}");
|
||||
}
|
||||
|
||||
// Generate alias for runtime type.
|
||||
let type_path = type_gen
|
||||
.resolve_type_path(input.id)
|
||||
.expect("runtime api input type is in metadata; qed")
|
||||
.to_token_stream(type_gen.settings());
|
||||
|
||||
Input {
|
||||
name: format_ident!("{name}"),
|
||||
type_alias: format_ident!("{alias}"),
|
||||
type_path,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
|
||||
let input_tuple_types = runtime_api_inputs
|
||||
.iter()
|
||||
.map(|i| {
|
||||
let ty = &i.type_alias;
|
||||
quote!(#method_name::#ty)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let input_args = runtime_api_inputs
|
||||
.iter()
|
||||
.map(|i| {
|
||||
let arg = &i.name;
|
||||
let ty = &i.type_alias;
|
||||
quote!(#arg: #method_name::#ty)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let input_param_names = runtime_api_inputs.iter().map(|i| &i.name);
|
||||
|
||||
let input_type_aliases = runtime_api_inputs.iter().map(|i| {
|
||||
let ty = &i.type_alias;
|
||||
let path = &i.type_path;
|
||||
quote!(pub type #ty = #path;)
|
||||
});
|
||||
|
||||
let output_type_path = type_gen
|
||||
.resolve_type_path(method.output_ty())?
|
||||
.to_token_stream(type_gen.settings());
|
||||
|
||||
// Define the input and output type bits for the method.
|
||||
let runtime_api_types = quote! {
|
||||
pub mod #method_name {
|
||||
use super::root_mod;
|
||||
use super::#types_mod_ident;
|
||||
|
||||
#(#input_type_aliases)*
|
||||
|
||||
pub mod output {
|
||||
use super::#types_mod_ident;
|
||||
pub type Output = #output_type_path;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Define the getter method that will live on the `ViewFunctionApi` type.
|
||||
let runtime_api_method = quote!(
|
||||
#docs
|
||||
pub fn #method_name(
|
||||
&self,
|
||||
#(#input_args),*
|
||||
) -> #crate_path::runtime_api::payload::StaticPayload<
|
||||
(#(#input_tuple_types,)*),
|
||||
#method_name::output::Output
|
||||
> {
|
||||
#crate_path::runtime_api::payload::StaticPayload::new_static(
|
||||
#trait_name_str,
|
||||
#method_name_str,
|
||||
(#(#input_param_names,)*),
|
||||
[#(#validation_hash,)*],
|
||||
)
|
||||
}
|
||||
);
|
||||
|
||||
Ok((runtime_api_types, runtime_api_method))
|
||||
})
|
||||
.collect::<Result<Vec<_>, CodegenError>>()?;
|
||||
|
||||
let trait_name = format_ident!("{}", trait_name_str);
|
||||
let types = types_and_methods.iter().map(|(types, _)| types);
|
||||
let methods = types_and_methods.iter().map(|(_, methods)| methods);
|
||||
|
||||
// The runtime API definition and types.
|
||||
let trait_defs = quote!(
|
||||
pub mod #trait_name_snake {
|
||||
use super::root_mod;
|
||||
use super::#types_mod_ident;
|
||||
|
||||
#docs
|
||||
pub struct #trait_name;
|
||||
|
||||
impl #trait_name {
|
||||
#( #methods )*
|
||||
}
|
||||
|
||||
#( #types )*
|
||||
}
|
||||
);
|
||||
|
||||
// A getter for the `RuntimeApi` to get the trait structure.
|
||||
let trait_getter = quote!(
|
||||
pub fn #trait_name_snake(&self) -> #trait_name_snake::#trait_name {
|
||||
#trait_name_snake::#trait_name
|
||||
}
|
||||
);
|
||||
|
||||
Ok((trait_defs, trait_getter))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::RuntimeGenerator;
|
||||
use frame_metadata::v15::{
|
||||
self, RuntimeApiMetadata, RuntimeApiMethodMetadata, RuntimeApiMethodParamMetadata,
|
||||
};
|
||||
use quote::quote;
|
||||
use scale_info::meta_type;
|
||||
use pezkuwi_subxt_metadata::Metadata;
|
||||
|
||||
fn metadata_with_runtime_apis(runtime_apis: Vec<RuntimeApiMetadata>) -> Metadata {
|
||||
let extrinsic_metadata = v15::ExtrinsicMetadata {
|
||||
version: 0,
|
||||
signed_extensions: vec![],
|
||||
address_ty: meta_type::<()>(),
|
||||
call_ty: meta_type::<()>(),
|
||||
signature_ty: meta_type::<()>(),
|
||||
extra_ty: meta_type::<()>(),
|
||||
};
|
||||
|
||||
let metadata: Metadata = v15::RuntimeMetadataV15::new(
|
||||
vec![],
|
||||
extrinsic_metadata,
|
||||
meta_type::<()>(),
|
||||
runtime_apis,
|
||||
v15::OuterEnums {
|
||||
call_enum_ty: meta_type::<()>(),
|
||||
event_enum_ty: meta_type::<()>(),
|
||||
error_enum_ty: meta_type::<()>(),
|
||||
},
|
||||
v15::CustomMetadata {
|
||||
map: Default::default(),
|
||||
},
|
||||
)
|
||||
.try_into()
|
||||
.expect("can build valid metadata");
|
||||
metadata
|
||||
}
|
||||
|
||||
fn generate_code(runtime_apis: Vec<RuntimeApiMetadata>) -> String {
|
||||
let metadata = metadata_with_runtime_apis(runtime_apis);
|
||||
let item_mod = syn::parse_quote!(
|
||||
pub mod api {}
|
||||
);
|
||||
let generator = RuntimeGenerator::new(metadata);
|
||||
let generated = generator
|
||||
.generate_runtime(
|
||||
item_mod,
|
||||
Default::default(),
|
||||
Default::default(),
|
||||
syn::parse_str("::subxt_path").unwrap(),
|
||||
false,
|
||||
)
|
||||
.expect("should be able to generate runtime");
|
||||
generated.to_string()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unique_param_names() {
|
||||
let runtime_apis = vec![RuntimeApiMetadata {
|
||||
name: "Test",
|
||||
methods: vec![RuntimeApiMethodMetadata {
|
||||
name: "test",
|
||||
inputs: vec![
|
||||
RuntimeApiMethodParamMetadata {
|
||||
name: "foo",
|
||||
ty: meta_type::<bool>(),
|
||||
},
|
||||
RuntimeApiMethodParamMetadata {
|
||||
name: "bar",
|
||||
ty: meta_type::<bool>(),
|
||||
},
|
||||
],
|
||||
output: meta_type::<bool>(),
|
||||
docs: vec![],
|
||||
}],
|
||||
|
||||
docs: vec![],
|
||||
}];
|
||||
|
||||
let code = generate_code(runtime_apis);
|
||||
|
||||
let expected_alias = quote!(
|
||||
pub mod test {
|
||||
use super::root_mod;
|
||||
use super::runtime_types;
|
||||
pub type Foo = ::core::primitive::bool;
|
||||
pub type Bar = ::core::primitive::bool;
|
||||
pub mod output {
|
||||
use super::runtime_types;
|
||||
pub type Output = ::core::primitive::bool;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
assert!(code.contains(&expected_alias.to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duplicate_param_names() {
|
||||
let runtime_apis = vec![RuntimeApiMetadata {
|
||||
name: "Test",
|
||||
methods: vec![RuntimeApiMethodMetadata {
|
||||
name: "test",
|
||||
inputs: vec![
|
||||
RuntimeApiMethodParamMetadata {
|
||||
name: "_a",
|
||||
ty: meta_type::<bool>(),
|
||||
},
|
||||
RuntimeApiMethodParamMetadata {
|
||||
name: "a",
|
||||
ty: meta_type::<bool>(),
|
||||
},
|
||||
RuntimeApiMethodParamMetadata {
|
||||
name: "__a",
|
||||
ty: meta_type::<bool>(),
|
||||
},
|
||||
],
|
||||
output: meta_type::<bool>(),
|
||||
docs: vec![],
|
||||
}],
|
||||
|
||||
docs: vec![],
|
||||
}];
|
||||
|
||||
let code = generate_code(runtime_apis);
|
||||
|
||||
let expected_alias = quote!(
|
||||
pub mod test {
|
||||
use super::root_mod;
|
||||
use super::runtime_types;
|
||||
pub type A = ::core::primitive::bool;
|
||||
pub type AParam1 = ::core::primitive::bool;
|
||||
pub type AParam2 = ::core::primitive::bool;
|
||||
pub mod output {
|
||||
use super::runtime_types;
|
||||
pub type Output = ::core::primitive::bool;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
assert!(code.contains(&expected_alias.to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duplicate_param_and_alias_names() {
|
||||
let runtime_apis = vec![RuntimeApiMetadata {
|
||||
name: "Test",
|
||||
methods: vec![RuntimeApiMethodMetadata {
|
||||
name: "test",
|
||||
inputs: vec![
|
||||
RuntimeApiMethodParamMetadata {
|
||||
name: "_",
|
||||
ty: meta_type::<bool>(),
|
||||
},
|
||||
RuntimeApiMethodParamMetadata {
|
||||
name: "_a",
|
||||
ty: meta_type::<bool>(),
|
||||
},
|
||||
RuntimeApiMethodParamMetadata {
|
||||
name: "_param_0",
|
||||
ty: meta_type::<bool>(),
|
||||
},
|
||||
RuntimeApiMethodParamMetadata {
|
||||
name: "__",
|
||||
ty: meta_type::<bool>(),
|
||||
},
|
||||
RuntimeApiMethodParamMetadata {
|
||||
name: "___param_0_param_2",
|
||||
ty: meta_type::<bool>(),
|
||||
},
|
||||
],
|
||||
output: meta_type::<bool>(),
|
||||
docs: vec![],
|
||||
}],
|
||||
|
||||
docs: vec![],
|
||||
}];
|
||||
|
||||
let code = generate_code(runtime_apis);
|
||||
|
||||
let expected_alias = quote!(
|
||||
pub mod test {
|
||||
use super::root_mod;
|
||||
use super::runtime_types;
|
||||
pub type Param0 = ::core::primitive::bool;
|
||||
pub type A = ::core::primitive::bool;
|
||||
pub type Param0Param2 = ::core::primitive::bool;
|
||||
pub type Param3 = ::core::primitive::bool;
|
||||
pub type Param0Param2Param4 = ::core::primitive::bool;
|
||||
pub mod output {
|
||||
use super::runtime_types;
|
||||
pub type Output = ::core::primitive::bool;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
assert!(code.contains(&expected_alias.to_string()));
|
||||
}
|
||||
}
|
||||
+239
@@ -0,0 +1,239 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
use heck::ToSnakeCase as _;
|
||||
use proc_macro2::TokenStream as TokenStream2;
|
||||
use quote::{format_ident, quote};
|
||||
use scale_typegen::TypeGenerator;
|
||||
use pezkuwi_subxt_metadata::{PalletMetadata, StorageEntryMetadata};
|
||||
|
||||
use super::CodegenError;
|
||||
|
||||
use scale_typegen::typegen::ir::ToTokensWithSettings;
|
||||
|
||||
/// Generate functions which create storage addresses from the provided pallet's metadata.
|
||||
/// These addresses can be used to access and iterate over storage values.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `type_gen` - [`scale_typegen::TypeGenerator`] that contains settings and all types from the runtime metadata.
|
||||
/// - `pallet` - Pallet metadata from which the storage items are generated.
|
||||
/// - `crate_path` - The crate path under which the `subxt-core` crate is located, e.g. `::pezkuwi_subxt::ext::pezkuwi_subxt_core` when using subxt as a dependency.
|
||||
pub fn generate_storage(
|
||||
type_gen: &TypeGenerator,
|
||||
pallet: &PalletMetadata,
|
||||
crate_path: &syn::Path,
|
||||
) -> Result<TokenStream2, CodegenError> {
|
||||
let Some(storage) = pallet.storage() else {
|
||||
// If there are no storage entries in this pallet, we
|
||||
// don't generate anything.
|
||||
return Ok(quote!());
|
||||
};
|
||||
|
||||
let storage_entries = storage
|
||||
.entries()
|
||||
.iter()
|
||||
.map(|entry| generate_storage_entry_fns(type_gen, pallet, entry, crate_path))
|
||||
.collect::<Result<Vec<_>, CodegenError>>()?;
|
||||
|
||||
let storage_entry_types = storage_entries.iter().map(|(types, _)| types);
|
||||
let storage_entry_methods = storage_entries.iter().map(|(_, method)| method);
|
||||
|
||||
let types_mod_ident = type_gen.types_mod_ident();
|
||||
|
||||
Ok(quote! {
|
||||
pub mod storage {
|
||||
use super::root_mod;
|
||||
use super::#types_mod_ident;
|
||||
|
||||
pub struct StorageApi;
|
||||
|
||||
impl StorageApi {
|
||||
#( #storage_entry_methods )*
|
||||
}
|
||||
|
||||
#( #storage_entry_types )*
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns storage entry functions and alias modules.
|
||||
fn generate_storage_entry_fns(
|
||||
type_gen: &TypeGenerator,
|
||||
pallet: &PalletMetadata,
|
||||
storage_entry: &StorageEntryMetadata,
|
||||
crate_path: &syn::Path,
|
||||
) -> Result<(TokenStream2, TokenStream2), CodegenError> {
|
||||
let types_mod_ident = type_gen.types_mod_ident();
|
||||
|
||||
let pallet_name = pallet.name();
|
||||
let storage_entry_name_str = storage_entry.name();
|
||||
let storage_entry_snake_case_name = storage_entry_name_str.to_snake_case();
|
||||
let storage_entry_snake_case_ident = format_ident!("{storage_entry_snake_case_name}");
|
||||
let Some(validation_hash) = pallet.storage_hash(storage_entry_name_str) else {
|
||||
return Err(CodegenError::MissingStorageMetadata(
|
||||
pallet_name.into(),
|
||||
storage_entry_name_str.into(),
|
||||
));
|
||||
};
|
||||
|
||||
let docs = storage_entry.docs();
|
||||
let docs: TokenStream2 = type_gen
|
||||
.settings()
|
||||
.should_gen_docs
|
||||
.then_some(quote! { #( #[doc = #docs ] )* })
|
||||
.unwrap_or_default();
|
||||
|
||||
struct Input {
|
||||
type_alias: syn::Ident,
|
||||
type_path: TokenStream2,
|
||||
}
|
||||
|
||||
let storage_key_types: Vec<Input> = storage_entry
|
||||
.keys()
|
||||
.enumerate()
|
||||
.map(|(idx, key)| {
|
||||
// Storage key aliases are just indexes; no names to use.
|
||||
let type_alias = format_ident!("Param{}", idx);
|
||||
|
||||
// Path to the actual type we'll have generated for this input.
|
||||
let type_path = type_gen
|
||||
.resolve_type_path(key.key_id)
|
||||
.expect("view function input type is in metadata; qed")
|
||||
.to_token_stream(type_gen.settings());
|
||||
|
||||
Input {
|
||||
type_alias,
|
||||
type_path,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let storage_key_tuple_types = storage_key_types
|
||||
.iter()
|
||||
.map(|i| {
|
||||
let ty = &i.type_alias;
|
||||
quote!(#storage_entry_snake_case_ident::#ty)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let storage_key_type_aliases = storage_key_types
|
||||
.iter()
|
||||
.map(|i| {
|
||||
let ty = &i.type_alias;
|
||||
let path = &i.type_path;
|
||||
quote!(pub type #ty = #path;)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let storage_value_type_path = type_gen
|
||||
.resolve_type_path(storage_entry.value_ty())?
|
||||
.to_token_stream(type_gen.settings());
|
||||
|
||||
let is_plain = if storage_entry.keys().len() == 0 {
|
||||
quote!(#crate_path::utils::Yes)
|
||||
} else {
|
||||
quote!(#crate_path::utils::Maybe)
|
||||
};
|
||||
|
||||
let storage_entry_types = quote!(
|
||||
pub mod #storage_entry_snake_case_ident {
|
||||
use super::root_mod;
|
||||
use super::#types_mod_ident;
|
||||
|
||||
#(#storage_key_type_aliases)*
|
||||
|
||||
pub mod output {
|
||||
use super::#types_mod_ident;
|
||||
pub type Output = #storage_value_type_path;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
let storage_entry_method = quote!(
|
||||
#docs
|
||||
pub fn #storage_entry_snake_case_ident(&self) -> #crate_path::storage::address::StaticAddress<
|
||||
(#(#storage_key_tuple_types,)*),
|
||||
#storage_entry_snake_case_ident::output::Output,
|
||||
#is_plain
|
||||
> {
|
||||
#crate_path::storage::address::StaticAddress::new_static(
|
||||
#pallet_name,
|
||||
#storage_entry_name_str,
|
||||
[#(#validation_hash,)*],
|
||||
)
|
||||
}
|
||||
);
|
||||
|
||||
Ok((storage_entry_types, storage_entry_method))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use frame_metadata::v15;
|
||||
use scale_info::{MetaType, meta_type};
|
||||
use pezkuwi_subxt_metadata::Metadata;
|
||||
|
||||
// TODO: Think about adding tests for storage codegen which can use this sort of function.
|
||||
#[allow(dead_code)]
|
||||
fn metadata_with_storage_entries(
|
||||
storage_entries: impl IntoIterator<Item = (&'static str, MetaType)>,
|
||||
) -> Metadata {
|
||||
let storage_entries: Vec<v15::StorageEntryMetadata> = storage_entries
|
||||
.into_iter()
|
||||
.map(|(name, key)| v15::StorageEntryMetadata {
|
||||
name,
|
||||
modifier: v15::StorageEntryModifier::Optional,
|
||||
ty: v15::StorageEntryType::Map {
|
||||
hashers: vec![v15::StorageHasher::Blake2_128Concat],
|
||||
key,
|
||||
value: meta_type::<bool>(),
|
||||
},
|
||||
default: vec![],
|
||||
docs: vec![],
|
||||
})
|
||||
.collect();
|
||||
|
||||
let pallet_1 = v15::PalletMetadata {
|
||||
name: "Pallet1",
|
||||
storage: Some(v15::PalletStorageMetadata {
|
||||
prefix: Default::default(),
|
||||
entries: storage_entries,
|
||||
}),
|
||||
calls: None,
|
||||
event: None,
|
||||
constants: vec![],
|
||||
error: None,
|
||||
index: 0,
|
||||
docs: vec![],
|
||||
};
|
||||
|
||||
let extrinsic_metadata = v15::ExtrinsicMetadata {
|
||||
version: 0,
|
||||
signed_extensions: vec![],
|
||||
address_ty: meta_type::<()>(),
|
||||
call_ty: meta_type::<()>(),
|
||||
signature_ty: meta_type::<()>(),
|
||||
extra_ty: meta_type::<()>(),
|
||||
};
|
||||
|
||||
let metadata: Metadata = v15::RuntimeMetadataV15::new(
|
||||
vec![pallet_1],
|
||||
extrinsic_metadata,
|
||||
meta_type::<()>(),
|
||||
vec![],
|
||||
v15::OuterEnums {
|
||||
call_enum_ty: meta_type::<()>(),
|
||||
event_enum_ty: meta_type::<()>(),
|
||||
error_enum_ty: meta_type::<()>(),
|
||||
},
|
||||
v15::CustomMetadata {
|
||||
map: Default::default(),
|
||||
},
|
||||
)
|
||||
.try_into()
|
||||
.expect("can build valid metadata");
|
||||
metadata
|
||||
}
|
||||
}
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! Errors that can be emitted from codegen.
|
||||
|
||||
use proc_macro2::{Span, TokenStream as TokenStream2};
|
||||
use scale_typegen::TypegenError;
|
||||
|
||||
/// Error returned when the Codegen cannot generate the runtime API.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[non_exhaustive]
|
||||
pub enum CodegenError {
|
||||
/// Cannot decode the metadata bytes.
|
||||
#[error("Could not decode metadata, only V14 and V15 metadata are supported: {0}")]
|
||||
Decode(#[from] codec::Error),
|
||||
/// Out of line modules are not supported.
|
||||
#[error(
|
||||
"Out-of-line subxt modules are not supported, make sure you are providing a body to your module: pub mod polkadot {{ ... }}"
|
||||
)]
|
||||
InvalidModule(Span),
|
||||
/// Invalid type path.
|
||||
#[error("Invalid type path {0}: {1}")]
|
||||
InvalidTypePath(String, syn::Error),
|
||||
/// Metadata for constant could not be found.
|
||||
#[error(
|
||||
"Metadata for constant entry {0}_{1} could not be found. Make sure you are providing a valid substrate-based metadata"
|
||||
)]
|
||||
MissingConstantMetadata(String, String),
|
||||
/// Metadata for storage could not be found.
|
||||
#[error(
|
||||
"Metadata for storage entry {0}_{1} could not be found. Make sure you are providing a valid substrate-based metadata"
|
||||
)]
|
||||
MissingStorageMetadata(String, String),
|
||||
/// Metadata for call could not be found.
|
||||
#[error(
|
||||
"Metadata for call entry {0}_{1} could not be found. Make sure you are providing a valid substrate-based metadata"
|
||||
)]
|
||||
MissingCallMetadata(String, String),
|
||||
/// Metadata for call could not be found.
|
||||
#[error(
|
||||
"Metadata for runtime API entry {0}_{1} could not be found. Make sure you are providing a valid substrate-based metadata"
|
||||
)]
|
||||
MissingRuntimeApiMetadata(String, String),
|
||||
/// Call variant must have all named fields.
|
||||
#[error(
|
||||
"Call variant for type {0} must have all named fields. Make sure you are providing a valid substrate-based metadata"
|
||||
)]
|
||||
InvalidCallVariant(u32),
|
||||
/// Type should be an variant/enum.
|
||||
#[error(
|
||||
"{0} type should be an variant/enum type. Make sure you are providing a valid substrate-based metadata"
|
||||
)]
|
||||
InvalidType(String),
|
||||
/// Extrinsic call type could not be found.
|
||||
#[error(
|
||||
"Extrinsic call type could not be found. Make sure you are providing a valid substrate-based metadata"
|
||||
)]
|
||||
MissingCallType,
|
||||
/// There are too many or too few hashers.
|
||||
#[error(
|
||||
"Could not generate functions for storage entry {storage_entry_name}. There are {key_count} keys, but only {hasher_count} hashers. The number of hashers must equal the number of keys or be exactly 1."
|
||||
)]
|
||||
InvalidStorageHasherCount {
|
||||
/// The name of the storage entry
|
||||
storage_entry_name: String,
|
||||
/// Number of keys
|
||||
key_count: usize,
|
||||
/// Number of hashers
|
||||
hasher_count: usize,
|
||||
},
|
||||
/// Cannot generate types.
|
||||
#[error("Type Generation failed: {0}")]
|
||||
TypeGeneration(#[from] TypegenError),
|
||||
/// Error when generating metadata from Wasm-runtime
|
||||
#[error("Failed to generate metadata from wasm file. reason: {0}")]
|
||||
Wasm(String),
|
||||
/// Other error.
|
||||
#[error("Other error: {0}")]
|
||||
Other(String),
|
||||
}
|
||||
|
||||
impl CodegenError {
|
||||
/// Fetch the location for this error.
|
||||
// Todo: Probably worth storing location outside of the variant,
|
||||
// so that there's a common way to set a location for some error.
|
||||
fn get_location(&self) -> Span {
|
||||
match self {
|
||||
Self::InvalidModule(span) => *span,
|
||||
Self::TypeGeneration(TypegenError::InvalidSubstitute(err)) => err.span,
|
||||
Self::InvalidTypePath(_, err) => err.span(),
|
||||
_ => proc_macro2::Span::call_site(),
|
||||
}
|
||||
}
|
||||
/// Render the error as an invocation of syn::compile_error!.
|
||||
pub fn into_compile_error(self) -> TokenStream2 {
|
||||
let msg = self.to_string();
|
||||
let span = self.get_location();
|
||||
syn::Error::new(span, msg).into_compile_error()
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
use crate::error::CodegenError;
|
||||
use syn::token;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct ItemMod {
|
||||
vis: syn::Visibility,
|
||||
mod_token: token::Mod,
|
||||
pub ident: syn::Ident,
|
||||
brace: token::Brace,
|
||||
items: Vec<syn::Item>,
|
||||
}
|
||||
|
||||
impl TryFrom<syn::ItemMod> for ItemMod {
|
||||
type Error = CodegenError;
|
||||
|
||||
fn try_from(module: syn::ItemMod) -> Result<Self, Self::Error> {
|
||||
let (brace, items) = match module.content {
|
||||
Some((brace, items)) => (brace, items),
|
||||
None => return Err(CodegenError::InvalidModule(module.ident.span())),
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
vis: module.vis,
|
||||
mod_token: module.mod_token,
|
||||
ident: module.ident,
|
||||
brace,
|
||||
items,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ItemMod {
|
||||
pub fn rust_items(&self) -> impl Iterator<Item = &syn::Item> {
|
||||
self.items.iter()
|
||||
}
|
||||
}
|
||||
+445
@@ -0,0 +1,445 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! Generate a type safe Subxt interface for a Substrate runtime from its metadata.
|
||||
//! This is used by the `#[subxt]` macro and `subxt codegen` CLI command, but can also
|
||||
//! be used directly if preferable.
|
||||
|
||||
#![deny(missing_docs)]
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
|
||||
mod api;
|
||||
pub mod error;
|
||||
mod ir;
|
||||
|
||||
#[cfg(feature = "web")]
|
||||
use getrandom as _;
|
||||
|
||||
use api::RuntimeGenerator;
|
||||
use proc_macro2::TokenStream as TokenStream2;
|
||||
use scale_typegen::typegen::settings::AllocCratePath;
|
||||
use scale_typegen::{
|
||||
DerivesRegistry, TypeGeneratorSettings, TypeSubstitutes, TypegenError,
|
||||
typegen::settings::substitutes::absolute_path,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use syn::parse_quote;
|
||||
|
||||
// Part of the public interface, so expose:
|
||||
pub use error::CodegenError;
|
||||
pub use pezkuwi_subxt_metadata::Metadata;
|
||||
pub use syn;
|
||||
|
||||
/// Generate a type safe interface to use with `subxt`.
|
||||
/// The options exposed here are similar to those exposed via
|
||||
/// the `#[subxt]` macro or via the `subxt codegen` CLI command.
|
||||
/// Both use this under the hood.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// Generating an interface using all of the defaults:
|
||||
///
|
||||
/// ```rust,standalone_crate
|
||||
/// use codec::Decode;
|
||||
/// use pezkuwi_subxt_codegen::{ Metadata, CodegenBuilder };
|
||||
///
|
||||
/// // Get hold of and decode some metadata:
|
||||
/// let encoded = std::fs::read("../artifacts/polkadot_metadata_full.scale").unwrap();
|
||||
/// let metadata = Metadata::decode(&mut &*encoded).unwrap();
|
||||
///
|
||||
/// // Generate a TokenStream representing the code for the interface.
|
||||
/// // This can be converted to a string, displayed as-is or output from a macro.
|
||||
/// let token_stream = CodegenBuilder::new().generate(metadata);
|
||||
/// ````
|
||||
pub struct CodegenBuilder {
|
||||
crate_path: syn::Path,
|
||||
use_default_derives: bool,
|
||||
use_default_substitutions: bool,
|
||||
generate_docs: bool,
|
||||
runtime_types_only: bool,
|
||||
item_mod: syn::ItemMod,
|
||||
extra_global_derives: Vec<syn::Path>,
|
||||
extra_global_attributes: Vec<syn::Attribute>,
|
||||
type_substitutes: HashMap<syn::Path, syn::Path>,
|
||||
derives_for_type: HashMap<syn::TypePath, Vec<syn::Path>>,
|
||||
attributes_for_type: HashMap<syn::TypePath, Vec<syn::Attribute>>,
|
||||
derives_for_type_recursive: HashMap<syn::TypePath, Vec<syn::Path>>,
|
||||
attributes_for_type_recursive: HashMap<syn::TypePath, Vec<syn::Attribute>>,
|
||||
}
|
||||
|
||||
impl Default for CodegenBuilder {
|
||||
fn default() -> Self {
|
||||
CodegenBuilder {
|
||||
crate_path: syn::parse_quote!(::pezkuwi_subxt::ext::pezkuwi_subxt_core),
|
||||
use_default_derives: true,
|
||||
use_default_substitutions: true,
|
||||
generate_docs: true,
|
||||
runtime_types_only: false,
|
||||
item_mod: syn::parse_quote!(
|
||||
pub mod api {}
|
||||
),
|
||||
extra_global_derives: Vec::new(),
|
||||
extra_global_attributes: Vec::new(),
|
||||
type_substitutes: HashMap::new(),
|
||||
derives_for_type: HashMap::new(),
|
||||
attributes_for_type: HashMap::new(),
|
||||
derives_for_type_recursive: HashMap::new(),
|
||||
attributes_for_type_recursive: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CodegenBuilder {
|
||||
/// Construct a builder to configure and generate a type-safe interface for Subxt.
|
||||
pub fn new() -> Self {
|
||||
CodegenBuilder::default()
|
||||
}
|
||||
|
||||
/// Disable the default derives that are applied to all types.
|
||||
///
|
||||
/// # Warning
|
||||
///
|
||||
/// This is not recommended, and is highly likely to break some part of the
|
||||
/// generated interface. Expect compile errors.
|
||||
pub fn disable_default_derives(&mut self) {
|
||||
self.use_default_derives = false;
|
||||
}
|
||||
|
||||
/// Disable the default type substitutions that are applied to the generated
|
||||
/// code.
|
||||
///
|
||||
/// # Warning
|
||||
///
|
||||
/// This is not recommended, and is highly likely to break some part of the
|
||||
/// generated interface. Expect compile errors.
|
||||
pub fn disable_default_substitutes(&mut self) {
|
||||
self.use_default_substitutions = false;
|
||||
}
|
||||
|
||||
/// Disable the output of doc comments associated with the generated types and
|
||||
/// methods. This can reduce the generated code size at the expense of losing
|
||||
/// documentation.
|
||||
pub fn no_docs(&mut self) {
|
||||
self.generate_docs = false;
|
||||
}
|
||||
|
||||
/// Only generate the types, and don't generate the rest of the Subxt specific
|
||||
/// interface.
|
||||
pub fn runtime_types_only(&mut self) {
|
||||
self.runtime_types_only = true;
|
||||
}
|
||||
|
||||
/// Set the additional derives that will be applied to all types. By default,
|
||||
/// a set of derives required for Subxt are automatically added for all types.
|
||||
///
|
||||
/// # Warning
|
||||
///
|
||||
/// Invalid derives, or derives that cannot be applied to _all_ of the generated
|
||||
/// types (taking into account that some types are substituted for hand written ones
|
||||
/// that we cannot add extra derives for) will lead to compile errors in the
|
||||
/// generated code.
|
||||
pub fn set_additional_global_derives(&mut self, derives: Vec<syn::Path>) {
|
||||
self.extra_global_derives = derives;
|
||||
}
|
||||
|
||||
/// Set the additional attributes that will be applied to all types. By default,
|
||||
/// a set of attributes required for Subxt are automatically added for all types.
|
||||
///
|
||||
/// # Warning
|
||||
///
|
||||
/// Invalid attributes can very easily lead to compile errors in the generated code.
|
||||
pub fn set_additional_global_attributes(&mut self, attributes: Vec<syn::Attribute>) {
|
||||
self.extra_global_attributes = attributes;
|
||||
}
|
||||
|
||||
/// Set additional derives for a specific type at the path given.
|
||||
///
|
||||
/// If you want to set the additional derives on all contained types recursively as well,
|
||||
/// you can set the `recursive` argument to `true`. If you don't do that,
|
||||
/// there might be compile errors in the generated code, if the derived trait
|
||||
/// relies on the fact that contained types also implement that trait.
|
||||
pub fn add_derives_for_type(
|
||||
&mut self,
|
||||
ty: syn::TypePath,
|
||||
derives: impl IntoIterator<Item = syn::Path>,
|
||||
recursive: bool,
|
||||
) {
|
||||
if recursive {
|
||||
self.derives_for_type_recursive
|
||||
.entry(ty)
|
||||
.or_default()
|
||||
.extend(derives);
|
||||
} else {
|
||||
self.derives_for_type.entry(ty).or_default().extend(derives);
|
||||
}
|
||||
}
|
||||
|
||||
/// Set additional attributes for a specific type at the path given.
|
||||
///
|
||||
/// Setting the `recursive` argument to `true` will additionally add the specified
|
||||
/// attributes to all contained types recursively.
|
||||
pub fn add_attributes_for_type(
|
||||
&mut self,
|
||||
ty: syn::TypePath,
|
||||
attributes: impl IntoIterator<Item = syn::Attribute>,
|
||||
recursive: bool,
|
||||
) {
|
||||
if recursive {
|
||||
self.attributes_for_type_recursive
|
||||
.entry(ty)
|
||||
.or_default()
|
||||
.extend(attributes);
|
||||
} else {
|
||||
self.attributes_for_type
|
||||
.entry(ty)
|
||||
.or_default()
|
||||
.extend(attributes);
|
||||
}
|
||||
}
|
||||
|
||||
/// Substitute a type at the given path with some type at the second path. During codegen,
|
||||
/// we will avoid generating the type at the first path given, and instead point any references
|
||||
/// to that type to the second path given.
|
||||
///
|
||||
/// The substituted type will need to implement the relevant traits to be compatible with the
|
||||
/// original, and it will need to SCALE encode and SCALE decode in a compatible way.
|
||||
pub fn set_type_substitute(&mut self, ty: syn::Path, with: syn::Path) {
|
||||
self.type_substitutes.insert(ty, with);
|
||||
}
|
||||
|
||||
/// By default, all of the code is generated inside a module `pub mod api {}`. We decorate
|
||||
/// this module with a few attributes to reduce compile warnings and things. You can provide a
|
||||
/// target module here, allowing you to add additional attributes or inner code items (with the
|
||||
/// warning that duplicate identifiers will lead to compile errors).
|
||||
pub fn set_target_module(&mut self, item_mod: syn::ItemMod) {
|
||||
self.item_mod = item_mod;
|
||||
}
|
||||
|
||||
/// Set the path to the `subxt` crate. By default, we expect it to be at `::pezkuwi_subxt::ext::pezkuwi_subxt_core`.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if the path provided is not an absolute path.
|
||||
pub fn set_subxt_crate_path(&mut self, crate_path: syn::Path) {
|
||||
if absolute_path(crate_path.clone()).is_err() {
|
||||
// Throw an error here, because otherwise we end up with a harder to comprehend error when
|
||||
// substitute types don't begin with an absolute path.
|
||||
panic!(
|
||||
"The provided crate path must be an absolute path, ie prefixed with '::' or 'crate'"
|
||||
);
|
||||
}
|
||||
self.crate_path = crate_path;
|
||||
}
|
||||
|
||||
/// Generate an interface, assuming that the default path to the `subxt` crate is `::pezkuwi_subxt::ext::pezkuwi_subxt_core`.
|
||||
/// If the `subxt` crate is not available as a top level dependency, use `generate` and provide
|
||||
/// a valid path to the `subxt¦ crate.
|
||||
pub fn generate(self, metadata: Metadata) -> Result<TokenStream2, CodegenError> {
|
||||
let crate_path = self.crate_path;
|
||||
|
||||
let mut derives_registry: DerivesRegistry = if self.use_default_derives {
|
||||
default_derives(&crate_path)
|
||||
} else {
|
||||
DerivesRegistry::new()
|
||||
};
|
||||
|
||||
derives_registry.add_derives_for_all(self.extra_global_derives);
|
||||
derives_registry.add_attributes_for_all(self.extra_global_attributes);
|
||||
|
||||
for (ty, derives) in self.derives_for_type {
|
||||
derives_registry.add_derives_for(ty, derives, false);
|
||||
}
|
||||
for (ty, derives) in self.derives_for_type_recursive {
|
||||
derives_registry.add_derives_for(ty, derives, true);
|
||||
}
|
||||
for (ty, attributes) in self.attributes_for_type {
|
||||
derives_registry.add_attributes_for(ty, attributes, false);
|
||||
}
|
||||
for (ty, attributes) in self.attributes_for_type_recursive {
|
||||
derives_registry.add_attributes_for(ty, attributes, true);
|
||||
}
|
||||
|
||||
let mut type_substitutes: TypeSubstitutes = if self.use_default_substitutions {
|
||||
default_substitutes(&crate_path)
|
||||
} else {
|
||||
TypeSubstitutes::new()
|
||||
};
|
||||
|
||||
for (from, with) in self.type_substitutes {
|
||||
let abs_path = absolute_path(with).map_err(TypegenError::from)?;
|
||||
type_substitutes
|
||||
.insert(from, abs_path)
|
||||
.map_err(TypegenError::from)?;
|
||||
}
|
||||
|
||||
let item_mod = self.item_mod;
|
||||
let generator = RuntimeGenerator::new(metadata);
|
||||
let should_gen_docs = self.generate_docs;
|
||||
|
||||
if self.runtime_types_only {
|
||||
generator.generate_runtime_types(
|
||||
item_mod,
|
||||
derives_registry,
|
||||
type_substitutes,
|
||||
crate_path,
|
||||
should_gen_docs,
|
||||
)
|
||||
} else {
|
||||
generator.generate_runtime(
|
||||
item_mod,
|
||||
derives_registry,
|
||||
type_substitutes,
|
||||
crate_path,
|
||||
should_gen_docs,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The default [`scale_typegen::TypeGeneratorSettings`], subxt is using for generating code.
|
||||
/// Useful for emulating subxt's code generation settings from e.g. subxt-explorer.
|
||||
pub fn default_subxt_type_gen_settings() -> TypeGeneratorSettings {
|
||||
let crate_path: syn::Path = parse_quote!(::pezkuwi_subxt::ext::pezkuwi_subxt_core);
|
||||
let derives = default_derives(&crate_path);
|
||||
let substitutes = default_substitutes(&crate_path);
|
||||
subxt_type_gen_settings(derives, substitutes, &crate_path, true)
|
||||
}
|
||||
|
||||
fn subxt_type_gen_settings(
|
||||
derives: scale_typegen::DerivesRegistry,
|
||||
substitutes: scale_typegen::TypeSubstitutes,
|
||||
crate_path: &syn::Path,
|
||||
should_gen_docs: bool,
|
||||
) -> TypeGeneratorSettings {
|
||||
// Are we using codec::Encode or codec::Decode derives?
|
||||
let are_codec_derives_used = derives.default_derives().derives().iter().any(|path| {
|
||||
let mut segments_backwards = path.segments.iter().rev();
|
||||
let ident = segments_backwards.next();
|
||||
let module = segments_backwards.next();
|
||||
|
||||
let is_ident_match = ident.is_some_and(|s| s.ident == "Encode" || s.ident == "Decode");
|
||||
let is_module_match = module.is_some_and(|s| s.ident == "codec");
|
||||
|
||||
is_ident_match && is_module_match
|
||||
});
|
||||
|
||||
// If we're inserting the codec derives, we also should use `CompactAs` where necessary.
|
||||
let compact_as_type_path =
|
||||
are_codec_derives_used.then(|| parse_quote!(#crate_path::ext::codec::CompactAs));
|
||||
|
||||
TypeGeneratorSettings {
|
||||
types_mod_ident: parse_quote!(runtime_types),
|
||||
should_gen_docs,
|
||||
derives,
|
||||
substitutes,
|
||||
decoded_bits_type_path: Some(parse_quote!(#crate_path::utils::bits::DecodedBits)),
|
||||
compact_as_type_path,
|
||||
compact_type_path: Some(parse_quote!(#crate_path::ext::codec::Compact)),
|
||||
alloc_crate_path: AllocCratePath::Custom(parse_quote!(#crate_path::alloc)),
|
||||
// Note: even when we don't use codec::Encode and codec::Decode, we need to keep #[codec(...)]
|
||||
// attributes because `#[codec(skip)]` is still used/important with `EncodeAsType` and `DecodeAsType`.
|
||||
insert_codec_attributes: true,
|
||||
}
|
||||
}
|
||||
|
||||
fn default_derives(crate_path: &syn::Path) -> DerivesRegistry {
|
||||
let encode_crate_path = quote::quote! { #crate_path::ext::scale_encode }.to_string();
|
||||
let decode_crate_path = quote::quote! { #crate_path::ext::scale_decode }.to_string();
|
||||
|
||||
let derives: [syn::Path; 3] = [
|
||||
parse_quote!(#crate_path::ext::scale_encode::EncodeAsType),
|
||||
parse_quote!(#crate_path::ext::scale_decode::DecodeAsType),
|
||||
parse_quote!(Debug),
|
||||
];
|
||||
|
||||
let attributes: [syn::Attribute; 2] = [
|
||||
parse_quote!(#[encode_as_type(crate_path = #encode_crate_path)]),
|
||||
parse_quote!(#[decode_as_type(crate_path = #decode_crate_path)]),
|
||||
];
|
||||
|
||||
let mut derives_registry = DerivesRegistry::new();
|
||||
derives_registry.add_derives_for_all(derives);
|
||||
derives_registry.add_attributes_for_all(attributes);
|
||||
derives_registry
|
||||
}
|
||||
|
||||
fn default_substitutes(crate_path: &syn::Path) -> TypeSubstitutes {
|
||||
let mut type_substitutes = TypeSubstitutes::new();
|
||||
|
||||
let defaults: [(syn::Path, syn::Path); 13] = [
|
||||
(
|
||||
parse_quote!(bitvec::order::Lsb0),
|
||||
parse_quote!(#crate_path::utils::bits::Lsb0),
|
||||
),
|
||||
(
|
||||
parse_quote!(bitvec::order::Msb0),
|
||||
parse_quote!(#crate_path::utils::bits::Msb0),
|
||||
),
|
||||
(
|
||||
parse_quote!(pezsp_core::crypto::AccountId32),
|
||||
parse_quote!(#crate_path::utils::AccountId32),
|
||||
),
|
||||
(
|
||||
parse_quote!(fp_account::AccountId20),
|
||||
parse_quote!(#crate_path::utils::AccountId20),
|
||||
),
|
||||
(
|
||||
parse_quote!(pezsp_runtime::multiaddress::MultiAddress),
|
||||
parse_quote!(#crate_path::utils::MultiAddress),
|
||||
),
|
||||
(
|
||||
parse_quote!(primitive_types::H160),
|
||||
parse_quote!(#crate_path::utils::H160),
|
||||
),
|
||||
(
|
||||
parse_quote!(primitive_types::H256),
|
||||
parse_quote!(#crate_path::utils::H256),
|
||||
),
|
||||
(
|
||||
parse_quote!(primitive_types::H512),
|
||||
parse_quote!(#crate_path::utils::H512),
|
||||
),
|
||||
(
|
||||
parse_quote!(pezframe_support::traits::misc::WrapperKeepOpaque),
|
||||
parse_quote!(#crate_path::utils::WrapperKeepOpaque),
|
||||
),
|
||||
// BTreeMap and BTreeSet impose an `Ord` constraint on their key types. This
|
||||
// can cause an issue with generated code that doesn't impl `Ord` by default.
|
||||
// Decoding them to Vec by default (KeyedVec is just an alias for Vec with
|
||||
// suitable type params) avoids these issues.
|
||||
(
|
||||
parse_quote!(BTreeMap),
|
||||
parse_quote!(#crate_path::utils::KeyedVec),
|
||||
),
|
||||
(
|
||||
parse_quote!(BinaryHeap),
|
||||
parse_quote!(#crate_path::alloc::vec::Vec),
|
||||
),
|
||||
(
|
||||
parse_quote!(BTreeSet),
|
||||
parse_quote!(#crate_path::alloc::vec::Vec),
|
||||
),
|
||||
// The `UncheckedExtrinsic(pub Vec<u8>)` is part of the runtime API calls.
|
||||
// The inner bytes represent the encoded extrinsic, however when deriving the
|
||||
// `EncodeAsType` the bytes would be re-encoded. This leads to the bytes
|
||||
// being altered by adding the length prefix in front of them.
|
||||
|
||||
// Note: Not sure if this is appropriate or not. The most recent polkadot.rs file does not have these.
|
||||
(
|
||||
parse_quote!(pezsp_runtime::generic::unchecked_extrinsic::UncheckedExtrinsic),
|
||||
parse_quote!(#crate_path::utils::UncheckedExtrinsic),
|
||||
),
|
||||
];
|
||||
|
||||
let defaults = defaults.into_iter().map(|(from, to)| {
|
||||
(
|
||||
from,
|
||||
absolute_path(to).expect("default substitutes above are absolute paths; qed"),
|
||||
)
|
||||
});
|
||||
type_substitutes
|
||||
.extend(defaults)
|
||||
.expect("default substitutes can always be parsed; qed");
|
||||
type_substitutes
|
||||
}
|
||||
Vendored
+83
@@ -0,0 +1,83 @@
|
||||
[package]
|
||||
name = "pezkuwi-subxt-core"
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
publish = true
|
||||
|
||||
license.workspace = true
|
||||
readme = "README.md"
|
||||
repository.workspace = true
|
||||
documentation.workspace = true
|
||||
homepage.workspace = true
|
||||
description = "A no-std compatible subset of Subxt's functionality"
|
||||
keywords = ["parity", "subxt", "extrinsic", "no-std"]
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
std = [
|
||||
"codec/std",
|
||||
"scale-info/std",
|
||||
"frame-metadata/std",
|
||||
"pezkuwi-subxt-metadata/std",
|
||||
"hex/std",
|
||||
"serde/std",
|
||||
"serde_json/std",
|
||||
"tracing/std",
|
||||
"impl-serde/std",
|
||||
"primitive-types/std",
|
||||
"pezsp-core/std",
|
||||
"pezsp-keyring/std",
|
||||
"pezsp-crypto-hashing/std",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
codec = { package = "parity-scale-codec", workspace = true, default-features = false, features = ["derive"] }
|
||||
frame-decode = { workspace = true }
|
||||
scale-info = { workspace = true, default-features = false, features = ["bit-vec"] }
|
||||
scale-value = { workspace = true, default-features = false }
|
||||
scale-bits = { workspace = true, default-features = false }
|
||||
scale-decode = { workspace = true, default-features = false, features = ["derive", "primitive-types"] }
|
||||
scale-encode = { workspace = true, default-features = false, features = ["derive", "primitive-types", "bits"] }
|
||||
frame-metadata = { workspace = true, default-features = false }
|
||||
pezkuwi-subxt-metadata = { workspace = true, default-features = false }
|
||||
derive-where = { workspace = true }
|
||||
hex = { workspace = true }
|
||||
serde = { workspace = true, default-features = false, features = ["derive"] }
|
||||
serde_json = { workspace = true, default-features = false, features = ["raw_value", "alloc"] }
|
||||
tracing = { workspace = true, default-features = false }
|
||||
pezsp-crypto-hashing = { workspace = true }
|
||||
hashbrown = { workspace = true }
|
||||
thiserror = { workspace = true, default-features = false }
|
||||
|
||||
# For ss58 encoding AccountId32 to serialize them properly:
|
||||
base58 = { workspace = true }
|
||||
blake2 = { workspace = true }
|
||||
|
||||
# Provides some deserialization, types like U256/H256 and hashing impls like twox/blake256:
|
||||
impl-serde = { workspace = true, default-features = false }
|
||||
primitive-types = { workspace = true, default-features = false, features = ["codec", "serde_no_std", "scale-info"] }
|
||||
|
||||
# AccountId20
|
||||
keccak-hash = { workspace = true}
|
||||
|
||||
[dev-dependencies]
|
||||
assert_matches = { workspace = true }
|
||||
bitvec = { workspace = true }
|
||||
codec = { workspace = true, features = ["derive", "bit-vec"] }
|
||||
pezkuwi-subxt-macro = { workspace = true }
|
||||
pezkuwi-subxt-signer = { workspace = true, features = ["sr25519", "subxt"] }
|
||||
pezsp-core = { workspace = true }
|
||||
pezsp-keyring = { workspace = true }
|
||||
hex = { workspace = true }
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
default-features = true
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[package.metadata.playground]
|
||||
default-features = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
# Subxt-Core
|
||||
|
||||
This library provides a no-std compatible subset of functionality that `subxt` and `subxt-signer` rely on.
|
||||
@@ -0,0 +1,155 @@
|
||||
// Copyright 2019-2024 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
use crate::config::TransactionExtension;
|
||||
use crate::config::transaction_extensions::{
|
||||
ChargeAssetTxPayment, ChargeTransactionPayment, CheckNonce,
|
||||
};
|
||||
use crate::dynamic::Value;
|
||||
use crate::error::ExtrinsicError;
|
||||
use crate::{Metadata, config::Config};
|
||||
use alloc::borrow::ToOwned;
|
||||
use frame_decode::extrinsics::ExtrinsicExtensions;
|
||||
use scale_decode::DecodeAsType;
|
||||
|
||||
/// The signed extensions of an extrinsic.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ExtrinsicTransactionExtensions<'a, T: Config> {
|
||||
bytes: &'a [u8],
|
||||
metadata: &'a Metadata,
|
||||
decoded_info: &'a ExtrinsicExtensions<'static, u32>,
|
||||
_marker: core::marker::PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<'a, T: Config> ExtrinsicTransactionExtensions<'a, T> {
|
||||
pub(crate) fn new(
|
||||
bytes: &'a [u8],
|
||||
metadata: &'a Metadata,
|
||||
decoded_info: &'a ExtrinsicExtensions<'static, u32>,
|
||||
) -> Self {
|
||||
Self {
|
||||
bytes,
|
||||
metadata,
|
||||
decoded_info,
|
||||
_marker: core::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns an iterator over each of the signed extension details of the extrinsic.
|
||||
pub fn iter(&self) -> impl Iterator<Item = ExtrinsicTransactionExtension<'a, T>> + use<'a, T> {
|
||||
self.decoded_info
|
||||
.iter()
|
||||
.map(|s| ExtrinsicTransactionExtension {
|
||||
bytes: &self.bytes[s.range()],
|
||||
ty_id: *s.ty(),
|
||||
identifier: s.name(),
|
||||
metadata: self.metadata,
|
||||
_marker: core::marker::PhantomData,
|
||||
})
|
||||
}
|
||||
|
||||
/// Searches through all signed extensions to find a specific one.
|
||||
/// If the Signed Extension is not found `Ok(None)` is returned.
|
||||
/// If the Signed Extension is found but decoding failed `Err(_)` is returned.
|
||||
pub fn find<S: TransactionExtension<T>>(&self) -> Result<Option<S::Decoded>, ExtrinsicError> {
|
||||
for ext in self.iter() {
|
||||
match ext.as_signed_extension::<S>() {
|
||||
// We found a match; return it:
|
||||
Ok(Some(e)) => return Ok(Some(e)),
|
||||
// No error, but no match either; next!
|
||||
Ok(None) => continue,
|
||||
// Error? return it
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// The tip of an extrinsic, extracted from the ChargeTransactionPayment or ChargeAssetTxPayment
|
||||
/// signed extension, depending on which is present.
|
||||
///
|
||||
/// Returns `None` if `tip` was not found or decoding failed.
|
||||
pub fn tip(&self) -> Option<u128> {
|
||||
// Note: the overhead of iterating multiple time should be negligible.
|
||||
self.find::<ChargeTransactionPayment>()
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(|e| e.tip())
|
||||
.or_else(|| {
|
||||
self.find::<ChargeAssetTxPayment<T>>()
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(|e| e.tip())
|
||||
})
|
||||
}
|
||||
|
||||
/// The nonce of the account that submitted the extrinsic, extracted from the CheckNonce signed extension.
|
||||
///
|
||||
/// Returns `None` if `nonce` was not found or decoding failed.
|
||||
pub fn nonce(&self) -> Option<u64> {
|
||||
self.find::<CheckNonce>().ok()?
|
||||
}
|
||||
}
|
||||
|
||||
/// A single signed extension
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ExtrinsicTransactionExtension<'a, T: Config> {
|
||||
bytes: &'a [u8],
|
||||
ty_id: u32,
|
||||
identifier: &'a str,
|
||||
metadata: &'a Metadata,
|
||||
_marker: core::marker::PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<'a, T: Config> ExtrinsicTransactionExtension<'a, T> {
|
||||
/// The bytes representing this signed extension.
|
||||
pub fn bytes(&self) -> &'a [u8] {
|
||||
self.bytes
|
||||
}
|
||||
|
||||
/// The name of the signed extension.
|
||||
pub fn name(&self) -> &'a str {
|
||||
self.identifier
|
||||
}
|
||||
|
||||
/// The type id of the signed extension.
|
||||
pub fn type_id(&self) -> u32 {
|
||||
self.ty_id
|
||||
}
|
||||
|
||||
/// Signed Extension as a [`scale_value::Value`]
|
||||
pub fn value(&self) -> Result<Value<u32>, ExtrinsicError> {
|
||||
let value = scale_value::scale::decode_as_type(
|
||||
&mut &self.bytes[..],
|
||||
self.ty_id,
|
||||
self.metadata.types(),
|
||||
)
|
||||
.map_err(|e| ExtrinsicError::CouldNotDecodeTransactionExtension {
|
||||
name: self.identifier.to_owned(),
|
||||
error: e.into(),
|
||||
})?;
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
/// Decodes the bytes of this Signed Extension into its associated `Decoded` type.
|
||||
/// Returns `Ok(None)` if the data we have doesn't match the Signed Extension we're asking to
|
||||
/// decode with.
|
||||
pub fn as_signed_extension<S: TransactionExtension<T>>(
|
||||
&self,
|
||||
) -> Result<Option<S::Decoded>, ExtrinsicError> {
|
||||
if !S::matches(self.identifier, self.ty_id, self.metadata.types()) {
|
||||
return Ok(None);
|
||||
}
|
||||
self.as_type::<S::Decoded>().map(Some)
|
||||
}
|
||||
|
||||
fn as_type<E: DecodeAsType>(&self) -> Result<E, ExtrinsicError> {
|
||||
let value = E::decode_as_type(&mut &self.bytes[..], self.ty_id, self.metadata.types())
|
||||
.map_err(|e| ExtrinsicError::CouldNotDecodeTransactionExtension {
|
||||
name: self.identifier.to_owned(),
|
||||
error: e,
|
||||
})?;
|
||||
Ok(value)
|
||||
}
|
||||
}
|
||||
+644
@@ -0,0 +1,644 @@
|
||||
// Copyright 2019-2024 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
use crate::blocks::extrinsic_transaction_extensions::ExtrinsicTransactionExtensions;
|
||||
use crate::{
|
||||
Metadata,
|
||||
config::{Config, HashFor, Hasher},
|
||||
error::{ExtrinsicDecodeErrorAt, ExtrinsicDecodeErrorAtReason, ExtrinsicError},
|
||||
};
|
||||
use alloc::sync::Arc;
|
||||
use alloc::vec::Vec;
|
||||
use frame_decode::extrinsics::Extrinsic;
|
||||
use scale_decode::{DecodeAsFields, DecodeAsType};
|
||||
|
||||
pub use crate::blocks::StaticExtrinsic;
|
||||
|
||||
/// The body of a block.
|
||||
pub struct Extrinsics<T: Config> {
|
||||
extrinsics: Vec<Arc<(Extrinsic<'static, u32>, Vec<u8>)>>,
|
||||
metadata: Metadata,
|
||||
hasher: T::Hasher,
|
||||
_marker: core::marker::PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T: Config> Extrinsics<T> {
|
||||
/// Instantiate a new [`Extrinsics`] object, given a vector containing
|
||||
/// each extrinsic hash (in the form of bytes) and some metadata that
|
||||
/// we'll use to decode them.
|
||||
pub fn decode_from(
|
||||
extrinsics: Vec<Vec<u8>>,
|
||||
metadata: Metadata,
|
||||
) -> Result<Self, ExtrinsicDecodeErrorAt> {
|
||||
let hasher = T::Hasher::new(&metadata);
|
||||
let extrinsics = extrinsics
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(extrinsic_index, bytes)| {
|
||||
let cursor = &mut &*bytes;
|
||||
|
||||
// Try to decode the extrinsic.
|
||||
let decoded_info =
|
||||
frame_decode::extrinsics::decode_extrinsic(cursor, &metadata, metadata.types())
|
||||
.map_err(|error| ExtrinsicDecodeErrorAt {
|
||||
extrinsic_index,
|
||||
error: ExtrinsicDecodeErrorAtReason::DecodeError(error),
|
||||
})?
|
||||
.into_owned();
|
||||
|
||||
// We didn't consume all bytes, so decoding probably failed.
|
||||
if !cursor.is_empty() {
|
||||
return Err(ExtrinsicDecodeErrorAt {
|
||||
extrinsic_index,
|
||||
error: ExtrinsicDecodeErrorAtReason::LeftoverBytes(cursor.to_vec()),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(Arc::new((decoded_info, bytes)))
|
||||
})
|
||||
.collect::<Result<_, ExtrinsicDecodeErrorAt>>()?;
|
||||
|
||||
Ok(Self {
|
||||
extrinsics,
|
||||
hasher,
|
||||
metadata,
|
||||
_marker: core::marker::PhantomData,
|
||||
})
|
||||
}
|
||||
|
||||
/// The number of extrinsics.
|
||||
pub fn len(&self) -> usize {
|
||||
self.extrinsics.len()
|
||||
}
|
||||
|
||||
/// Are there no extrinsics in this block?
|
||||
// Note: mainly here to satisfy clippy.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.extrinsics.is_empty()
|
||||
}
|
||||
|
||||
/// Returns an iterator over the extrinsics in the block body.
|
||||
// Dev note: The returned iterator is 'static + Send so that we can box it up and make
|
||||
// use of it with our `FilterExtrinsic` stuff.
|
||||
pub fn iter(&self) -> impl Iterator<Item = ExtrinsicDetails<T>> + Send + Sync + 'static {
|
||||
let extrinsics = self.extrinsics.clone();
|
||||
let num_extrinsics = self.extrinsics.len();
|
||||
let hasher = self.hasher;
|
||||
let metadata = self.metadata.clone();
|
||||
|
||||
(0..num_extrinsics).map(move |index| {
|
||||
ExtrinsicDetails::new(
|
||||
index as u32,
|
||||
extrinsics[index].clone(),
|
||||
hasher,
|
||||
metadata.clone(),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Iterate through the extrinsics using metadata to dynamically decode and skip
|
||||
/// them, and return only those which should decode to the provided `E` type.
|
||||
/// If an error occurs, all subsequent iterations return `None`.
|
||||
pub fn find<E: StaticExtrinsic>(
|
||||
&self,
|
||||
) -> impl Iterator<Item = Result<FoundExtrinsic<T, E>, ExtrinsicError>> {
|
||||
self.iter().filter_map(|details| {
|
||||
match details.as_extrinsic::<E>() {
|
||||
// Failed to decode extrinsic:
|
||||
Err(err) => Some(Err(err)),
|
||||
// Extrinsic for a different pallet / different call (skip):
|
||||
Ok(None) => None,
|
||||
Ok(Some(value)) => Some(Ok(FoundExtrinsic { details, value })),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Iterate through the extrinsics using metadata to dynamically decode and skip
|
||||
/// them, and return the first extrinsic found which decodes to the provided `E` type.
|
||||
pub fn find_first<E: StaticExtrinsic>(
|
||||
&self,
|
||||
) -> Result<Option<FoundExtrinsic<T, E>>, ExtrinsicError> {
|
||||
self.find::<E>().next().transpose()
|
||||
}
|
||||
|
||||
/// Iterate through the extrinsics using metadata to dynamically decode and skip
|
||||
/// them, and return the last extrinsic found which decodes to the provided `Ev` type.
|
||||
pub fn find_last<E: StaticExtrinsic>(
|
||||
&self,
|
||||
) -> Result<Option<FoundExtrinsic<T, E>>, ExtrinsicError> {
|
||||
self.find::<E>().last().transpose()
|
||||
}
|
||||
|
||||
/// Find an extrinsics that decodes to the type provided. Returns true if it was found.
|
||||
pub fn has<E: StaticExtrinsic>(&self) -> Result<bool, ExtrinsicError> {
|
||||
Ok(self.find::<E>().next().transpose()?.is_some())
|
||||
}
|
||||
}
|
||||
|
||||
/// A single extrinsic in a block.
|
||||
pub struct ExtrinsicDetails<T: Config> {
|
||||
/// The index of the extrinsic in the block.
|
||||
index: u32,
|
||||
/// Extrinsic bytes and decode info.
|
||||
ext: Arc<(Extrinsic<'static, u32>, Vec<u8>)>,
|
||||
/// Hash the extrinsic if we want.
|
||||
hasher: T::Hasher,
|
||||
/// Subxt metadata to fetch the extrinsic metadata.
|
||||
metadata: Metadata,
|
||||
_marker: core::marker::PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T> ExtrinsicDetails<T>
|
||||
where
|
||||
T: Config,
|
||||
{
|
||||
// Attempt to dynamically decode a single extrinsic from the given input.
|
||||
#[doc(hidden)]
|
||||
pub fn new(
|
||||
index: u32,
|
||||
ext: Arc<(Extrinsic<'static, u32>, Vec<u8>)>,
|
||||
hasher: T::Hasher,
|
||||
metadata: Metadata,
|
||||
) -> ExtrinsicDetails<T> {
|
||||
ExtrinsicDetails {
|
||||
index,
|
||||
ext,
|
||||
hasher,
|
||||
metadata,
|
||||
_marker: core::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate and return the hash of the extrinsic, based on the configured hasher.
|
||||
pub fn hash(&self) -> HashFor<T> {
|
||||
// Use hash(), not hash_of(), because we don't want to double encode the bytes.
|
||||
self.hasher.hash(self.bytes())
|
||||
}
|
||||
|
||||
/// Is the extrinsic signed?
|
||||
pub fn is_signed(&self) -> bool {
|
||||
self.decoded_info().is_signed()
|
||||
}
|
||||
|
||||
/// The index of the extrinsic in the block.
|
||||
pub fn index(&self) -> u32 {
|
||||
self.index
|
||||
}
|
||||
|
||||
/// Return _all_ of the bytes representing this extrinsic, which include, in order:
|
||||
/// - First byte: abbbbbbb (a = 0 for unsigned, 1 for signed, b = version)
|
||||
/// - SignatureType (if the payload is signed)
|
||||
/// - Address
|
||||
/// - Signature
|
||||
/// - Extra fields
|
||||
/// - Extrinsic call bytes
|
||||
pub fn bytes(&self) -> &[u8] {
|
||||
&self.ext.1
|
||||
}
|
||||
|
||||
/// Return only the bytes representing this extrinsic call:
|
||||
/// - First byte is the pallet index
|
||||
/// - Second byte is the variant (call) index
|
||||
/// - Followed by field bytes.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// Please use [`Self::bytes`] if you want to get all extrinsic bytes.
|
||||
pub fn call_bytes(&self) -> &[u8] {
|
||||
&self.bytes()[self.decoded_info().call_data_range()]
|
||||
}
|
||||
|
||||
/// Return the bytes representing the fields stored in this extrinsic.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// This is a subset of [`Self::call_bytes`] that does not include the
|
||||
/// first two bytes that denote the pallet index and the variant index.
|
||||
pub fn field_bytes(&self) -> &[u8] {
|
||||
// Note: this cannot panic because we checked the extrinsic bytes
|
||||
// to contain at least two bytes.
|
||||
&self.bytes()[self.decoded_info().call_data_args_range()]
|
||||
}
|
||||
|
||||
/// Return only the bytes of the address that signed this extrinsic.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// Returns `None` if the extrinsic is not signed.
|
||||
pub fn address_bytes(&self) -> Option<&[u8]> {
|
||||
self.decoded_info()
|
||||
.signature_payload()
|
||||
.map(|s| &self.bytes()[s.address_range()])
|
||||
}
|
||||
|
||||
/// Returns Some(signature_bytes) if the extrinsic was signed otherwise None is returned.
|
||||
pub fn signature_bytes(&self) -> Option<&[u8]> {
|
||||
self.decoded_info()
|
||||
.signature_payload()
|
||||
.map(|s| &self.bytes()[s.signature_range()])
|
||||
}
|
||||
|
||||
/// Returns the signed extension `extra` bytes of the extrinsic.
|
||||
/// Each signed extension has an `extra` type (May be zero-sized).
|
||||
/// These bytes are the scale encoded `extra` fields of each signed extension in order of the signed extensions.
|
||||
/// They do *not* include the `additional` signed bytes that are used as part of the payload that is signed.
|
||||
///
|
||||
/// Note: Returns `None` if the extrinsic is not signed.
|
||||
pub fn transaction_extensions_bytes(&self) -> Option<&[u8]> {
|
||||
self.decoded_info()
|
||||
.transaction_extension_payload()
|
||||
.map(|t| &self.bytes()[t.range()])
|
||||
}
|
||||
|
||||
/// Returns `None` if the extrinsic is not signed.
|
||||
pub fn transaction_extensions(&self) -> Option<ExtrinsicTransactionExtensions<'_, T>> {
|
||||
self.decoded_info()
|
||||
.transaction_extension_payload()
|
||||
.map(|t| ExtrinsicTransactionExtensions::new(self.bytes(), &self.metadata, t))
|
||||
}
|
||||
|
||||
/// The index of the pallet that the extrinsic originated from.
|
||||
pub fn pallet_index(&self) -> u8 {
|
||||
self.decoded_info().pallet_index()
|
||||
}
|
||||
|
||||
/// The index of the extrinsic variant that the extrinsic originated from.
|
||||
pub fn call_index(&self) -> u8 {
|
||||
self.decoded_info().call_index()
|
||||
}
|
||||
|
||||
/// The name of the pallet from whence the extrinsic originated.
|
||||
pub fn pallet_name(&self) -> &str {
|
||||
self.decoded_info().pallet_name()
|
||||
}
|
||||
|
||||
/// The name of the call (ie the name of the variant that it corresponds to).
|
||||
pub fn call_name(&self) -> &str {
|
||||
self.decoded_info().call_name()
|
||||
}
|
||||
|
||||
/// Decode and provide the extrinsic fields back in the form of a [`scale_value::Composite`]
|
||||
/// type which represents the named or unnamed fields that were present in the extrinsic.
|
||||
pub fn decode_as_fields<E: DecodeAsFields>(&self) -> Result<E, ExtrinsicError> {
|
||||
let bytes = &mut self.field_bytes();
|
||||
let mut fields = self.decoded_info().call_data().map(|d| {
|
||||
let name = if d.name().is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(d.name())
|
||||
};
|
||||
scale_decode::Field::new(*d.ty(), name)
|
||||
});
|
||||
let decoded =
|
||||
E::decode_as_fields(bytes, &mut fields, self.metadata.types()).map_err(|e| {
|
||||
ExtrinsicError::CannotDecodeFields {
|
||||
extrinsic_index: self.index as usize,
|
||||
error: e,
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok(decoded)
|
||||
}
|
||||
|
||||
/// Attempt to decode these [`ExtrinsicDetails`] into a type representing the extrinsic fields.
|
||||
/// Such types are exposed in the codegen as `pallet_name::calls::types::CallName` types.
|
||||
pub fn as_extrinsic<E: StaticExtrinsic>(&self) -> Result<Option<E>, ExtrinsicError> {
|
||||
if self.decoded_info().pallet_name() == E::PALLET
|
||||
&& self.decoded_info().call_name() == E::CALL
|
||||
{
|
||||
let mut fields = self.decoded_info().call_data().map(|d| {
|
||||
let name = if d.name().is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(d.name())
|
||||
};
|
||||
scale_decode::Field::new(*d.ty(), name)
|
||||
});
|
||||
let decoded =
|
||||
E::decode_as_fields(&mut self.field_bytes(), &mut fields, self.metadata.types())
|
||||
.map_err(|e| ExtrinsicError::CannotDecodeFields {
|
||||
extrinsic_index: self.index as usize,
|
||||
error: e,
|
||||
})?;
|
||||
Ok(Some(decoded))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempt to decode these [`ExtrinsicDetails`] into an outer call enum type (which includes
|
||||
/// the pallet and extrinsic enum variants as well as the extrinsic fields). A compatible
|
||||
/// type for this is exposed via static codegen as a root level `Call` type.
|
||||
pub fn as_root_extrinsic<E: DecodeAsType>(&self) -> Result<E, ExtrinsicError> {
|
||||
let decoded = E::decode_as_type(
|
||||
&mut &self.call_bytes()[..],
|
||||
self.metadata.outer_enums().call_enum_ty(),
|
||||
self.metadata.types(),
|
||||
)
|
||||
.map_err(|e| ExtrinsicError::CannotDecodeIntoRootExtrinsic {
|
||||
extrinsic_index: self.index as usize,
|
||||
error: e,
|
||||
})?;
|
||||
|
||||
Ok(decoded)
|
||||
}
|
||||
|
||||
fn decoded_info(&self) -> &Extrinsic<'static, u32> {
|
||||
&self.ext.0
|
||||
}
|
||||
}
|
||||
|
||||
/// A Static Extrinsic found in a block coupled with it's details.
|
||||
pub struct FoundExtrinsic<T: Config, E> {
|
||||
/// Details for the extrinsic.
|
||||
pub details: ExtrinsicDetails<T>,
|
||||
/// The decoded extrinsic value.
|
||||
pub value: E,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::config::SubstrateConfig;
|
||||
use assert_matches::assert_matches;
|
||||
use codec::{Decode, Encode};
|
||||
use frame_metadata::v15::{CustomMetadata, OuterEnums};
|
||||
use frame_metadata::{
|
||||
RuntimeMetadataPrefixed,
|
||||
v15::{ExtrinsicMetadata, PalletCallMetadata, PalletMetadata, RuntimeMetadataV15},
|
||||
};
|
||||
use scale_info::{TypeInfo, meta_type};
|
||||
use scale_value::Value;
|
||||
|
||||
// Extrinsic needs to contain at least the generic type parameter "Call"
|
||||
// for the metadata to be valid.
|
||||
// The "Call" type from the metadata is used to decode extrinsics.
|
||||
#[allow(unused)]
|
||||
#[derive(TypeInfo)]
|
||||
struct ExtrinsicType<Address, Call, Signature, Extra> {
|
||||
pub signature: Option<(Address, Signature, Extra)>,
|
||||
pub function: Call,
|
||||
}
|
||||
|
||||
// Because this type is used to decode extrinsics, we expect this to be a TypeDefVariant.
|
||||
// Each pallet must contain one single variant.
|
||||
#[allow(unused)]
|
||||
#[derive(
|
||||
Encode,
|
||||
Decode,
|
||||
TypeInfo,
|
||||
Clone,
|
||||
Debug,
|
||||
PartialEq,
|
||||
Eq,
|
||||
scale_encode::EncodeAsType,
|
||||
scale_decode::DecodeAsType,
|
||||
)]
|
||||
enum RuntimeCall {
|
||||
Test(Pallet),
|
||||
}
|
||||
|
||||
// The calls of the pallet.
|
||||
#[allow(unused)]
|
||||
#[derive(
|
||||
Encode,
|
||||
Decode,
|
||||
TypeInfo,
|
||||
Clone,
|
||||
Debug,
|
||||
PartialEq,
|
||||
Eq,
|
||||
scale_encode::EncodeAsType,
|
||||
scale_decode::DecodeAsType,
|
||||
)]
|
||||
enum Pallet {
|
||||
#[allow(unused)]
|
||||
#[codec(index = 2)]
|
||||
TestCall {
|
||||
value: u128,
|
||||
signed: bool,
|
||||
name: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
#[derive(
|
||||
Encode,
|
||||
Decode,
|
||||
TypeInfo,
|
||||
Clone,
|
||||
Debug,
|
||||
PartialEq,
|
||||
Eq,
|
||||
scale_encode::EncodeAsType,
|
||||
scale_decode::DecodeAsType,
|
||||
)]
|
||||
struct TestCallExtrinsic {
|
||||
value: u128,
|
||||
signed: bool,
|
||||
name: String,
|
||||
}
|
||||
|
||||
impl StaticExtrinsic for TestCallExtrinsic {
|
||||
const PALLET: &'static str = "Test";
|
||||
const CALL: &'static str = "TestCall";
|
||||
}
|
||||
|
||||
/// Build fake metadata consisting the types needed to represent an extrinsic.
|
||||
fn metadata() -> Metadata {
|
||||
let pallets = vec![PalletMetadata {
|
||||
name: "Test",
|
||||
storage: None,
|
||||
calls: Some(PalletCallMetadata {
|
||||
ty: meta_type::<Pallet>(),
|
||||
}),
|
||||
event: None,
|
||||
constants: vec![],
|
||||
error: None,
|
||||
index: 0,
|
||||
docs: vec![],
|
||||
}];
|
||||
|
||||
let extrinsic = ExtrinsicMetadata {
|
||||
version: 4,
|
||||
signed_extensions: vec![],
|
||||
address_ty: meta_type::<()>(),
|
||||
call_ty: meta_type::<RuntimeCall>(),
|
||||
signature_ty: meta_type::<()>(),
|
||||
extra_ty: meta_type::<()>(),
|
||||
};
|
||||
|
||||
let meta = RuntimeMetadataV15::new(
|
||||
pallets,
|
||||
extrinsic,
|
||||
meta_type::<()>(),
|
||||
vec![],
|
||||
OuterEnums {
|
||||
call_enum_ty: meta_type::<RuntimeCall>(),
|
||||
event_enum_ty: meta_type::<()>(),
|
||||
error_enum_ty: meta_type::<()>(),
|
||||
},
|
||||
CustomMetadata {
|
||||
map: Default::default(),
|
||||
},
|
||||
);
|
||||
let runtime_metadata: RuntimeMetadataPrefixed = meta.into();
|
||||
let metadata: pezkuwi_subxt_metadata::Metadata = runtime_metadata.try_into().unwrap();
|
||||
|
||||
metadata
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extrinsic_metadata_consistency() {
|
||||
let metadata = metadata();
|
||||
|
||||
// Except our metadata to contain the registered types.
|
||||
let pallet = metadata.pallet_by_call_index(0).expect("pallet exists");
|
||||
let extrinsic = pallet
|
||||
.call_variant_by_index(2)
|
||||
.expect("metadata contains the RuntimeCall enum with this pallet");
|
||||
|
||||
assert_eq!(pallet.name(), "Test");
|
||||
assert_eq!(&extrinsic.name, "TestCall");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insufficient_extrinsic_bytes() {
|
||||
let metadata = metadata();
|
||||
|
||||
// Decode with empty bytes.
|
||||
let result = Extrinsics::<SubstrateConfig>::decode_from(vec![vec![]], metadata);
|
||||
assert_matches!(
|
||||
result.err(),
|
||||
Some(crate::error::ExtrinsicDecodeErrorAt {
|
||||
extrinsic_index: 0,
|
||||
error: _
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_version_extrinsic() {
|
||||
use frame_decode::extrinsics::ExtrinsicDecodeError;
|
||||
|
||||
let metadata = metadata();
|
||||
|
||||
// Decode with invalid version.
|
||||
let result = Extrinsics::<SubstrateConfig>::decode_from(vec![vec![3u8].encode()], metadata);
|
||||
|
||||
assert_matches!(
|
||||
result.err(),
|
||||
Some(crate::error::ExtrinsicDecodeErrorAt {
|
||||
extrinsic_index: 0,
|
||||
error: ExtrinsicDecodeErrorAtReason::DecodeError(
|
||||
ExtrinsicDecodeError::VersionNotSupported(3)
|
||||
),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tx_hashes_line_up() {
|
||||
let metadata = metadata();
|
||||
let hasher = <SubstrateConfig as Config>::Hasher::new(&metadata);
|
||||
|
||||
let tx = crate::dynamic::tx(
|
||||
"Test",
|
||||
"TestCall",
|
||||
vec![
|
||||
Value::u128(10),
|
||||
Value::bool(true),
|
||||
Value::string("SomeValue"),
|
||||
],
|
||||
);
|
||||
|
||||
// Encoded TX ready to submit.
|
||||
let tx_encoded = crate::tx::create_v4_unsigned::<SubstrateConfig, _>(&tx, &metadata)
|
||||
.expect("Valid dynamic parameters are provided");
|
||||
|
||||
// Extrinsic details ready to decode.
|
||||
let extrinsics = Extrinsics::<SubstrateConfig>::decode_from(
|
||||
vec![tx_encoded.encoded().to_owned()],
|
||||
metadata,
|
||||
)
|
||||
.expect("Valid extrinsic");
|
||||
|
||||
let extrinsic = extrinsics.iter().next().unwrap();
|
||||
|
||||
// Both of these types should produce the same bytes.
|
||||
assert_eq!(tx_encoded.encoded(), extrinsic.bytes(), "bytes should eq");
|
||||
// Both of these types should produce the same hash.
|
||||
assert_eq!(
|
||||
tx_encoded.hash_with(hasher),
|
||||
extrinsic.hash(),
|
||||
"hashes should eq"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn statically_decode_extrinsic() {
|
||||
let metadata = metadata();
|
||||
|
||||
let tx = crate::dynamic::tx(
|
||||
"Test",
|
||||
"TestCall",
|
||||
vec![
|
||||
Value::u128(10),
|
||||
Value::bool(true),
|
||||
Value::string("SomeValue"),
|
||||
],
|
||||
);
|
||||
let tx_encoded = crate::tx::create_v4_unsigned::<SubstrateConfig, _>(&tx, &metadata)
|
||||
.expect("Valid dynamic parameters are provided");
|
||||
|
||||
// Note: `create_unsigned` produces the extrinsic bytes by prefixing the extrinsic length.
|
||||
// The length is handled deserializing `ChainBlockExtrinsic`, therefore the first byte is not needed.
|
||||
let extrinsics = Extrinsics::<SubstrateConfig>::decode_from(
|
||||
vec![tx_encoded.encoded().to_owned()],
|
||||
metadata,
|
||||
)
|
||||
.expect("Valid extrinsic");
|
||||
|
||||
let extrinsic = extrinsics.iter().next().unwrap();
|
||||
|
||||
assert!(!extrinsic.is_signed());
|
||||
|
||||
assert_eq!(extrinsic.index(), 0);
|
||||
|
||||
assert_eq!(extrinsic.pallet_index(), 0);
|
||||
assert_eq!(extrinsic.pallet_name(), "Test");
|
||||
|
||||
assert_eq!(extrinsic.call_index(), 2);
|
||||
assert_eq!(extrinsic.call_name(), "TestCall");
|
||||
|
||||
// Decode the extrinsic to the root enum.
|
||||
let decoded_extrinsic = extrinsic
|
||||
.as_root_extrinsic::<RuntimeCall>()
|
||||
.expect("can decode extrinsic to root enum");
|
||||
|
||||
assert_eq!(
|
||||
decoded_extrinsic,
|
||||
RuntimeCall::Test(Pallet::TestCall {
|
||||
value: 10,
|
||||
signed: true,
|
||||
name: "SomeValue".into(),
|
||||
})
|
||||
);
|
||||
|
||||
// Decode the extrinsic to the extrinsic variant.
|
||||
let decoded_extrinsic = extrinsic
|
||||
.as_extrinsic::<TestCallExtrinsic>()
|
||||
.expect("can decode extrinsic to extrinsic variant")
|
||||
.expect("value cannot be None");
|
||||
|
||||
assert_eq!(
|
||||
decoded_extrinsic,
|
||||
TestCallExtrinsic {
|
||||
value: 10,
|
||||
signed: true,
|
||||
name: "SomeValue".into(),
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
// Copyright 2019-2024 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! Decode and iterate over the extrinsics in block bodies.
|
||||
//!
|
||||
//! Use the [`decode_from`] function as an entry point to decoding extrinsics, and then
|
||||
//! have a look at [`Extrinsics`] and [`ExtrinsicDetails`] to see which methods are available
|
||||
//! to work with the extrinsics.
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! ```rust
|
||||
//! extern crate alloc;
|
||||
//!
|
||||
//! use pezkuwi_subxt_macro::subxt;
|
||||
//! use pezkuwi_subxt_core::blocks;
|
||||
//! use pezkuwi_subxt_core::Metadata;
|
||||
//! use pezkuwi_subxt_core::config::PolkadotConfig;
|
||||
//! use alloc::vec;
|
||||
//!
|
||||
//! // If we generate types without `subxt`, we need to point to `::pezkuwi_subxt_core`:
|
||||
//! #[subxt(
|
||||
//! crate = "::pezkuwi_subxt_core",
|
||||
//! runtime_metadata_path = "../artifacts/polkadot_metadata_small.scale",
|
||||
//! )]
|
||||
//! pub mod polkadot {}
|
||||
//!
|
||||
//! // Some metadata we'd like to use to help us decode extrinsics:
|
||||
//! let metadata_bytes = include_bytes!("../../../artifacts/polkadot_metadata_small.scale");
|
||||
//! let metadata = Metadata::decode_from(&metadata_bytes[..]).unwrap();
|
||||
//!
|
||||
//! // Some extrinsics we'd like to decode:
|
||||
//! let ext_bytes = vec![
|
||||
//! hex::decode("1004020000").unwrap(),
|
||||
//! hex::decode("c10184001cbd2d43530a44705ad088af313e18f80b53ef16b36177cd4b77b846f2a5f07c01a27c400241aeafdea1871b32f1f01e92acd272ddfe6b2f8b73b64c606572a530c470a94ef654f7baa5828474754a1fe31b59f91f6bb5c2cd5a07c22d4b8b8387350100000000001448656c6c6f").unwrap(),
|
||||
//! hex::decode("550284001cbd2d43530a44705ad088af313e18f80b53ef16b36177cd4b77b846f2a5f07c0144bb92734447c893ab16d520fae0d455257550efa28ee66bf6dc942cb8b00d5d2799b98bc2865d21812278a9a266acd7352f40742ff11a6ce1f400013961598485010000000400008eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a481700505a4f7e9f4eb106").unwrap()
|
||||
//! ];
|
||||
//!
|
||||
//! // Given some chain config and metadata, we know how to decode the bytes.
|
||||
//! let exts = blocks::decode_from::<PolkadotConfig>(ext_bytes, metadata).unwrap();
|
||||
//!
|
||||
//! // We'll see 3 extrinsics:
|
||||
//! assert_eq!(exts.len(), 3);
|
||||
//!
|
||||
//! // We can iterate over them and decode various details out of them.
|
||||
//! for ext in exts.iter() {
|
||||
//! println!("Pallet: {}", ext.pallet_name());
|
||||
//! println!("Call: {}", ext.call_name());
|
||||
//! }
|
||||
//!
|
||||
//! # let ext_details: Vec<_> = exts.iter()
|
||||
//! # .map(|ext| {
|
||||
//! # let pallet = ext.pallet_name().to_string();
|
||||
//! # let call = ext.call_name().to_string();
|
||||
//! # (pallet, call)
|
||||
//! # })
|
||||
//! # .collect();
|
||||
//! #
|
||||
//! # assert_eq!(ext_details, vec![
|
||||
//! # ("Timestamp".to_owned(), "set".to_owned()),
|
||||
//! # ("System".to_owned(), "remark".to_owned()),
|
||||
//! # ("Balances".to_owned(), "transfer_allow_death".to_owned()),
|
||||
//! # ]);
|
||||
//! ```
|
||||
|
||||
mod extrinsic_transaction_extensions;
|
||||
mod extrinsics;
|
||||
mod static_extrinsic;
|
||||
|
||||
use crate::Metadata;
|
||||
use crate::config::Config;
|
||||
use crate::error::ExtrinsicDecodeErrorAt;
|
||||
pub use crate::error::ExtrinsicError;
|
||||
use alloc::vec::Vec;
|
||||
pub use extrinsic_transaction_extensions::{
|
||||
ExtrinsicTransactionExtension, ExtrinsicTransactionExtensions,
|
||||
};
|
||||
pub use extrinsics::{ExtrinsicDetails, Extrinsics, FoundExtrinsic};
|
||||
pub use static_extrinsic::StaticExtrinsic;
|
||||
|
||||
/// Instantiate a new [`Extrinsics`] object, given a vector containing each extrinsic hash (in the
|
||||
/// form of bytes) and some metadata that we'll use to decode them.
|
||||
///
|
||||
/// This is a shortcut for [`Extrinsics::decode_from`].
|
||||
pub fn decode_from<T: Config>(
|
||||
extrinsics: Vec<Vec<u8>>,
|
||||
metadata: Metadata,
|
||||
) -> Result<Extrinsics<T>, ExtrinsicDecodeErrorAt> {
|
||||
Extrinsics::decode_from(extrinsics, metadata)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// Copyright 2019-2024 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
use scale_decode::DecodeAsFields;
|
||||
|
||||
/// Trait to uniquely identify the extrinsic's identity from the runtime metadata.
|
||||
///
|
||||
/// Generated API structures that represent an extrinsic implement this trait.
|
||||
///
|
||||
/// The trait is utilized to decode emitted extrinsics from a block, via obtaining the
|
||||
/// form of the `Extrinsic` from the metadata.
|
||||
pub trait StaticExtrinsic: DecodeAsFields {
|
||||
/// Pallet name.
|
||||
const PALLET: &'static str;
|
||||
/// Call name.
|
||||
const CALL: &'static str;
|
||||
|
||||
/// Returns true if the given pallet and call names match this extrinsic.
|
||||
fn is_extrinsic(pallet: &str, call: &str) -> bool {
|
||||
Self::PALLET == pallet && Self::CALL == call
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
// Copyright 2019-2024 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! A couple of client types that we use elsewhere.
|
||||
|
||||
use crate::{
|
||||
Metadata,
|
||||
config::{Config, HashFor},
|
||||
};
|
||||
use derive_where::derive_where;
|
||||
|
||||
/// This provides access to some relevant client state in transaction extensions,
|
||||
/// and is just a combination of some of the available properties.
|
||||
#[derive_where(Clone, Debug)]
|
||||
pub struct ClientState<C: Config> {
|
||||
/// Genesis hash.
|
||||
pub genesis_hash: HashFor<C>,
|
||||
/// Runtime version.
|
||||
pub runtime_version: RuntimeVersion,
|
||||
/// Metadata.
|
||||
pub metadata: Metadata,
|
||||
}
|
||||
|
||||
/// Runtime version information needed to submit transactions.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct RuntimeVersion {
|
||||
/// Version of the runtime specification. A full-node will not attempt to use its native
|
||||
/// runtime in substitute for the on-chain Wasm runtime unless all of `spec_name`,
|
||||
/// `spec_version` and `authoring_version` are the same between Wasm and native.
|
||||
pub spec_version: u32,
|
||||
/// All existing dispatches are fully compatible when this number doesn't change. If this
|
||||
/// number changes, then `spec_version` must change, also.
|
||||
///
|
||||
/// This number must change when an existing dispatchable (module ID, dispatch ID) is changed,
|
||||
/// either through an alteration in its user-level semantics, a parameter
|
||||
/// added/removed/changed, a dispatchable being removed, a module being removed, or a
|
||||
/// dispatchable/module changing its index.
|
||||
///
|
||||
/// It need *not* change when a new module is added or when a dispatchable is added.
|
||||
pub transaction_version: u32,
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
// Copyright 2019-2024 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
use crate::config::transaction_extensions::CheckMortalityParams;
|
||||
|
||||
use super::{Config, HashFor};
|
||||
use super::{ExtrinsicParams, transaction_extensions};
|
||||
|
||||
/// The default [`super::ExtrinsicParams`] implementation understands common signed extensions
|
||||
/// and how to apply them to a given chain.
|
||||
pub type DefaultExtrinsicParams<T> = transaction_extensions::AnyOf<
|
||||
T,
|
||||
(
|
||||
transaction_extensions::VerifySignature<T>,
|
||||
transaction_extensions::CheckSpecVersion,
|
||||
transaction_extensions::CheckTxVersion,
|
||||
transaction_extensions::CheckNonce,
|
||||
transaction_extensions::CheckGenesis<T>,
|
||||
transaction_extensions::CheckMortality<T>,
|
||||
transaction_extensions::ChargeAssetTxPayment<T>,
|
||||
transaction_extensions::ChargeTransactionPayment,
|
||||
transaction_extensions::CheckMetadataHash,
|
||||
),
|
||||
>;
|
||||
|
||||
/// A builder that outputs the set of [`super::ExtrinsicParams::Params`] required for
|
||||
/// [`DefaultExtrinsicParams`]. This may expose methods that aren't applicable to the current
|
||||
/// chain; such values will simply be ignored if so.
|
||||
pub struct DefaultExtrinsicParamsBuilder<T: Config> {
|
||||
/// `None` means the tx will be immortal, else it's mortality is described.
|
||||
mortality: transaction_extensions::CheckMortalityParams<T>,
|
||||
/// `None` means the nonce will be automatically set.
|
||||
nonce: Option<u64>,
|
||||
/// `None` means we'll use the native token.
|
||||
tip_of_asset_id: Option<T::AssetId>,
|
||||
tip: u128,
|
||||
tip_of: u128,
|
||||
}
|
||||
|
||||
impl<T: Config> Default for DefaultExtrinsicParamsBuilder<T> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
mortality: CheckMortalityParams::default(),
|
||||
tip: 0,
|
||||
tip_of: 0,
|
||||
tip_of_asset_id: None,
|
||||
nonce: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> DefaultExtrinsicParamsBuilder<T> {
|
||||
/// Configure new extrinsic params. We default to providing no tip
|
||||
/// and using an immortal transaction unless otherwise configured
|
||||
pub fn new() -> Self {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
/// Make the transaction immortal, meaning it will never expire. This means that it could, in
|
||||
/// theory, be pending for a long time and only be included many blocks into the future.
|
||||
pub fn immortal(mut self) -> Self {
|
||||
self.mortality = transaction_extensions::CheckMortalityParams::immortal();
|
||||
self
|
||||
}
|
||||
|
||||
/// Make the transaction mortal, given a number of blocks it will be mortal for from
|
||||
/// the current block at the time of submission.
|
||||
///
|
||||
/// # Warning
|
||||
///
|
||||
/// This will ultimately return an error if used for creating extrinsic offline, because we need
|
||||
/// additional information in order to set the mortality properly.
|
||||
///
|
||||
/// When creating offline transactions, you must use [`Self::mortal_from_unchecked`] instead to set
|
||||
/// the mortality. This provides all of the necessary information which we must otherwise be online
|
||||
/// in order to obtain.
|
||||
pub fn mortal(mut self, for_n_blocks: u64) -> Self {
|
||||
self.mortality = transaction_extensions::CheckMortalityParams::mortal(for_n_blocks);
|
||||
self
|
||||
}
|
||||
|
||||
/// Configure a transaction that will be mortal for the number of blocks given, and from the
|
||||
/// block details provided. Prefer to use [`Self::mortal()`] where possible, which prevents
|
||||
/// the block number and hash from being misaligned.
|
||||
pub fn mortal_from_unchecked(
|
||||
mut self,
|
||||
for_n_blocks: u64,
|
||||
from_block_n: u64,
|
||||
from_block_hash: HashFor<T>,
|
||||
) -> Self {
|
||||
self.mortality = transaction_extensions::CheckMortalityParams::mortal_from_unchecked(
|
||||
for_n_blocks,
|
||||
from_block_n,
|
||||
from_block_hash,
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
/// Provide a specific nonce for the submitter of the extrinsic
|
||||
pub fn nonce(mut self, nonce: u64) -> Self {
|
||||
self.nonce = Some(nonce);
|
||||
self
|
||||
}
|
||||
|
||||
/// Provide a tip to the block author in the chain's native token.
|
||||
pub fn tip(mut self, tip: u128) -> Self {
|
||||
self.tip = tip;
|
||||
self.tip_of = tip;
|
||||
self.tip_of_asset_id = None;
|
||||
self
|
||||
}
|
||||
|
||||
/// Provide a tip to the block author using the token denominated by the `asset_id` provided. This
|
||||
/// is not applicable on chains which don't use the `ChargeAssetTxPayment` signed extension; in this
|
||||
/// case, no tip will be given.
|
||||
pub fn tip_of(mut self, tip: u128, asset_id: T::AssetId) -> Self {
|
||||
self.tip = 0;
|
||||
self.tip_of = tip;
|
||||
self.tip_of_asset_id = Some(asset_id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the extrinsic parameters.
|
||||
pub fn build(self) -> <DefaultExtrinsicParams<T> as ExtrinsicParams<T>>::Params {
|
||||
let check_mortality_params = self.mortality;
|
||||
|
||||
let charge_asset_tx_params = if let Some(asset_id) = self.tip_of_asset_id {
|
||||
transaction_extensions::ChargeAssetTxPaymentParams::tip_of(self.tip, asset_id)
|
||||
} else {
|
||||
transaction_extensions::ChargeAssetTxPaymentParams::tip(self.tip)
|
||||
};
|
||||
|
||||
let charge_transaction_params =
|
||||
transaction_extensions::ChargeTransactionPaymentParams::tip(self.tip);
|
||||
|
||||
let check_nonce_params = if let Some(nonce) = self.nonce {
|
||||
transaction_extensions::CheckNonceParams::with_nonce(nonce)
|
||||
} else {
|
||||
transaction_extensions::CheckNonceParams::from_chain()
|
||||
};
|
||||
|
||||
(
|
||||
(),
|
||||
(),
|
||||
(),
|
||||
check_nonce_params,
|
||||
(),
|
||||
check_mortality_params,
|
||||
charge_asset_tx_params,
|
||||
charge_transaction_params,
|
||||
(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
fn assert_default<T: Default>(_t: T) {}
|
||||
|
||||
#[test]
|
||||
fn params_are_default() {
|
||||
let params = DefaultExtrinsicParamsBuilder::<crate::config::PolkadotConfig>::new().build();
|
||||
assert_default(params)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
// Copyright 2019-2024 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! This module contains a trait which controls the parameters that must
|
||||
//! be provided in order to successfully construct an extrinsic.
|
||||
//! [`crate::config::DefaultExtrinsicParams`] provides a general-purpose
|
||||
//! implementation of this that will work in many cases.
|
||||
|
||||
use crate::{
|
||||
client::ClientState,
|
||||
config::{Config, HashFor},
|
||||
error::ExtrinsicParamsError,
|
||||
};
|
||||
use alloc::vec::Vec;
|
||||
use core::any::Any;
|
||||
|
||||
/// This trait allows you to configure the "signed extra" and
|
||||
/// "additional" parameters that are a part of the transaction payload
|
||||
/// or the signer payload respectively.
|
||||
pub trait ExtrinsicParams<T: Config>: ExtrinsicParamsEncoder + Sized + Send + 'static {
|
||||
/// These parameters can be provided to the constructor along with
|
||||
/// some default parameters that `subxt` understands, in order to
|
||||
/// help construct your [`ExtrinsicParams`] object.
|
||||
type Params: Params<T>;
|
||||
|
||||
/// Construct a new instance of our [`ExtrinsicParams`].
|
||||
fn new(client: &ClientState<T>, params: Self::Params) -> Result<Self, ExtrinsicParamsError>;
|
||||
}
|
||||
|
||||
/// This trait is expected to be implemented for any [`ExtrinsicParams`], and
|
||||
/// defines how to encode the "additional" and "extra" params. Both functions
|
||||
/// are optional and will encode nothing by default.
|
||||
pub trait ExtrinsicParamsEncoder: 'static {
|
||||
/// This is expected to SCALE encode the transaction extension data to some
|
||||
/// buffer that has been provided. This data is attached to the transaction
|
||||
/// and also (by default) attached to the signer payload which is signed to
|
||||
/// provide a signature for the transaction.
|
||||
///
|
||||
/// If [`ExtrinsicParamsEncoder::encode_signer_payload_value_to`] is implemented,
|
||||
/// then that will be used instead when generating a signer payload. Useful for
|
||||
/// eg the `VerifySignature` extension, which is send with the transaction but
|
||||
/// is not a part of the signer payload.
|
||||
fn encode_value_to(&self, _v: &mut Vec<u8>) {}
|
||||
|
||||
/// See [`ExtrinsicParamsEncoder::encode_value_to`]. This defaults to calling that
|
||||
/// method, but if implemented will dictate what is encoded to the signer payload.
|
||||
fn encode_signer_payload_value_to(&self, v: &mut Vec<u8>) {
|
||||
self.encode_value_to(v);
|
||||
}
|
||||
|
||||
/// This is expected to SCALE encode the "implicit" (formally "additional")
|
||||
/// parameters to some buffer that has been provided. These parameters are
|
||||
/// _not_ sent along with the transaction, but are taken into account when
|
||||
/// signing it, meaning the client and node must agree on their values.
|
||||
fn encode_implicit_to(&self, _v: &mut Vec<u8>) {}
|
||||
|
||||
/// Set the signature. This happens after we have constructed the extrinsic params,
|
||||
/// and so is defined here rather than on the params, below. We need to use `&dyn Any`
|
||||
/// to keep this trait object safe, but can downcast in the impls.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Implementations of this will likely try to downcast the provided `account_id`
|
||||
/// and `signature` into `T::AccountId` and `T::Signature` (where `T: Config`), and are
|
||||
/// free to panic if this downcasting does not succeed.
|
||||
///
|
||||
/// In typical usage, this is not a problem, since this method is only called internally
|
||||
/// and provided values which line up with the relevant `Config`. In theory though, this
|
||||
/// method can be called manually with any types, hence this warning.
|
||||
fn inject_signature(&mut self, _account_id: &dyn Any, _signature: &dyn Any) {}
|
||||
}
|
||||
|
||||
/// The parameters (ie [`ExtrinsicParams::Params`]) can also have data injected into them,
|
||||
/// allowing Subxt to retrieve data from the chain and amend the parameters with it when
|
||||
/// online.
|
||||
pub trait Params<T: Config> {
|
||||
/// Set the account nonce.
|
||||
fn inject_account_nonce(&mut self, _nonce: u64) {}
|
||||
/// Set the current block.
|
||||
fn inject_block(&mut self, _number: u64, _hash: HashFor<T>) {}
|
||||
}
|
||||
|
||||
impl<T: Config> Params<T> for () {}
|
||||
|
||||
macro_rules! impl_tuples {
|
||||
($($ident:ident $index:tt),+) => {
|
||||
impl <Conf: Config, $($ident : Params<Conf>),+> Params<Conf> for ($($ident,)+){
|
||||
fn inject_account_nonce(&mut self, nonce: u64) {
|
||||
$(self.$index.inject_account_nonce(nonce);)+
|
||||
}
|
||||
|
||||
fn inject_block(&mut self, number: u64, hash: HashFor<Conf>) {
|
||||
$(self.$index.inject_block(number, hash);)+
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[rustfmt::skip]
|
||||
const _: () = {
|
||||
impl_tuples!(A 0);
|
||||
impl_tuples!(A 0, B 1);
|
||||
impl_tuples!(A 0, B 1, C 2);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3, E 4);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13, O 14);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13, O 14, P 15);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13, O 14, P 15, Q 16);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13, O 14, P 15, Q 16, R 17);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13, O 14, P 15, Q 16, R 17, S 18);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13, O 14, P 15, Q 16, R 17, S 18, T 19);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13, O 14, P 15, Q 16, R 17, S 18, T 19, U 20);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13, O 14, P 15, Q 16, R 17, S 18, T 19, U 20, V 21);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13, O 14, P 15, Q 16, R 17, S 18, T 19, U 20, V 21, W 22);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13, O 14, P 15, Q 16, R 17, S 18, T 19, U 20, V 21, W 22, X 23);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13, O 14, P 15, Q 16, R 17, S 18, T 19, U 20, V 21, W 22, X 23, Y 24);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13, O 14, P 15, Q 16, R 17, S 18, T 19, U 20, V 21, W 22, X 23, Y 24, Z 25);
|
||||
};
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
// Copyright 2019-2024 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! This module provides a [`Config`] type, which is used to define various
|
||||
//! types that are important in order to speak to a particular chain.
|
||||
//! [`SubstrateConfig`] provides a default set of these types suitable for the
|
||||
//! default Substrate node implementation, and [`PolkadotConfig`] for a
|
||||
//! Polkadot node.
|
||||
|
||||
mod default_extrinsic_params;
|
||||
mod extrinsic_params;
|
||||
|
||||
pub mod polkadot;
|
||||
pub mod substrate;
|
||||
pub mod transaction_extensions;
|
||||
|
||||
use codec::{Decode, Encode};
|
||||
use core::fmt::Debug;
|
||||
use scale_decode::DecodeAsType;
|
||||
use scale_encode::EncodeAsType;
|
||||
use serde::{Serialize, de::DeserializeOwned};
|
||||
use pezkuwi_subxt_metadata::Metadata;
|
||||
|
||||
pub use default_extrinsic_params::{DefaultExtrinsicParams, DefaultExtrinsicParamsBuilder};
|
||||
pub use extrinsic_params::{ExtrinsicParams, ExtrinsicParamsEncoder};
|
||||
pub use polkadot::{PolkadotConfig, PolkadotExtrinsicParams, PolkadotExtrinsicParamsBuilder};
|
||||
pub use substrate::{SubstrateConfig, SubstrateExtrinsicParams, SubstrateExtrinsicParamsBuilder};
|
||||
pub use transaction_extensions::TransactionExtension;
|
||||
|
||||
/// Runtime types.
|
||||
// Note: the `Send + Sync + 'static` bound isn't strictly required, but currently deriving
|
||||
// TypeInfo automatically applies a 'static bound to all generic types (including this one),
|
||||
// And we want the compiler to infer `Send` and `Sync` OK for things which have `T: Config`
|
||||
// rather than having to `unsafe impl` them ourselves.
|
||||
pub trait Config: Sized + Send + Sync + 'static {
|
||||
/// The account ID type.
|
||||
type AccountId: Debug + Clone + Encode + Decode + Serialize + Send;
|
||||
|
||||
/// The address type.
|
||||
type Address: Debug + Encode + From<Self::AccountId>;
|
||||
|
||||
/// The signature type.
|
||||
type Signature: Debug + Clone + Encode + Decode + Send;
|
||||
|
||||
/// The hashing system (algorithm) being used in the runtime (e.g. Blake2).
|
||||
type Hasher: Debug + Clone + Copy + Hasher + Send + Sync;
|
||||
|
||||
/// The block header.
|
||||
type Header: Debug + Header<Hasher = Self::Hasher> + Sync + Send + DeserializeOwned + Clone;
|
||||
|
||||
/// This type defines the extrinsic extra and additional parameters.
|
||||
type ExtrinsicParams: ExtrinsicParams<Self>;
|
||||
|
||||
/// This is used to identify an asset in the `ChargeAssetTxPayment` signed extension.
|
||||
type AssetId: Debug + Clone + Encode + DecodeAsType + EncodeAsType + Send;
|
||||
}
|
||||
|
||||
/// Given some [`Config`], this returns the type of hash used.
|
||||
pub type HashFor<T> = <<T as Config>::Hasher as Hasher>::Output;
|
||||
|
||||
/// given some [`Config`], this return the other params needed for its `ExtrinsicParams`.
|
||||
pub type ParamsFor<T> = <<T as Config>::ExtrinsicParams as ExtrinsicParams<T>>::Params;
|
||||
|
||||
/// Block hashes must conform to a bunch of things to be used in Subxt.
|
||||
pub trait Hash:
|
||||
Debug
|
||||
+ Copy
|
||||
+ Send
|
||||
+ Sync
|
||||
+ Decode
|
||||
+ AsRef<[u8]>
|
||||
+ Serialize
|
||||
+ DeserializeOwned
|
||||
+ Encode
|
||||
+ PartialEq
|
||||
+ Eq
|
||||
+ core::hash::Hash
|
||||
{
|
||||
}
|
||||
impl<T> Hash for T where
|
||||
T: Debug
|
||||
+ Copy
|
||||
+ Send
|
||||
+ Sync
|
||||
+ Decode
|
||||
+ AsRef<[u8]>
|
||||
+ Serialize
|
||||
+ DeserializeOwned
|
||||
+ Encode
|
||||
+ PartialEq
|
||||
+ Eq
|
||||
+ core::hash::Hash
|
||||
{
|
||||
}
|
||||
|
||||
/// This represents the hasher used by a node to hash things like block headers
|
||||
/// and extrinsics.
|
||||
pub trait Hasher {
|
||||
/// The type given back from the hash operation
|
||||
type Output: Hash;
|
||||
|
||||
/// Construct a new hasher.
|
||||
fn new(metadata: &Metadata) -> Self;
|
||||
|
||||
/// Hash some bytes to the given output type.
|
||||
fn hash(&self, s: &[u8]) -> Self::Output;
|
||||
|
||||
/// Hash some SCALE encodable type to the given output type.
|
||||
fn hash_of<S: Encode>(&self, s: &S) -> Self::Output {
|
||||
let out = s.encode();
|
||||
self.hash(&out)
|
||||
}
|
||||
}
|
||||
|
||||
/// This represents the block header type used by a node.
|
||||
pub trait Header: Sized + Encode + Decode {
|
||||
/// The block number type for this header.
|
||||
type Number: Into<u64>;
|
||||
/// The hasher used to hash this header.
|
||||
type Hasher: Hasher;
|
||||
|
||||
/// Return the block number of this header.
|
||||
fn number(&self) -> Self::Number;
|
||||
|
||||
/// Hash this header.
|
||||
fn hash_with(&self, hasher: Self::Hasher) -> <Self::Hasher as Hasher>::Output {
|
||||
hasher.hash_of(self)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// Copyright 2019-2024 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! Polkadot specific configuration
|
||||
|
||||
use super::{Config, DefaultExtrinsicParams, DefaultExtrinsicParamsBuilder};
|
||||
|
||||
use crate::config::SubstrateConfig;
|
||||
pub use crate::utils::{AccountId32, MultiAddress, MultiSignature};
|
||||
pub use primitive_types::{H256, U256};
|
||||
|
||||
/// Default set of commonly used types by Polkadot nodes.
|
||||
// Note: The trait implementations exist just to make life easier,
|
||||
// but shouldn't strictly be necessary since users can't instantiate this type.
|
||||
#[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
|
||||
pub enum PolkadotConfig {}
|
||||
|
||||
impl Config for PolkadotConfig {
|
||||
type AccountId = <SubstrateConfig as Config>::AccountId;
|
||||
type Signature = <SubstrateConfig as Config>::Signature;
|
||||
type Hasher = <SubstrateConfig as Config>::Hasher;
|
||||
type Header = <SubstrateConfig as Config>::Header;
|
||||
type AssetId = <SubstrateConfig as Config>::AssetId;
|
||||
|
||||
// Address on Polkadot has no account index, whereas it's u32 on
|
||||
// the default substrate dev node.
|
||||
type Address = MultiAddress<Self::AccountId, ()>;
|
||||
|
||||
// These are the same as the default substrate node, but redefined
|
||||
// because we need to pass the PolkadotConfig trait as a param.
|
||||
type ExtrinsicParams = PolkadotExtrinsicParams<Self>;
|
||||
}
|
||||
|
||||
/// A struct representing the signed extra and additional parameters required
|
||||
/// to construct a transaction for a polkadot node.
|
||||
pub type PolkadotExtrinsicParams<T> = DefaultExtrinsicParams<T>;
|
||||
|
||||
/// A builder which leads to [`PolkadotExtrinsicParams`] being constructed.
|
||||
/// This is what you provide to methods like `sign_and_submit()`.
|
||||
pub type PolkadotExtrinsicParamsBuilder<T> = DefaultExtrinsicParamsBuilder<T>;
|
||||
+396
@@ -0,0 +1,396 @@
|
||||
// Copyright 2019-2024 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! Substrate specific configuration
|
||||
|
||||
use super::{Config, DefaultExtrinsicParams, DefaultExtrinsicParamsBuilder, Hasher, Header};
|
||||
pub use crate::utils::{AccountId32, MultiAddress, MultiSignature};
|
||||
use alloc::format;
|
||||
use alloc::vec::Vec;
|
||||
use codec::{Decode, Encode};
|
||||
pub use primitive_types::{H256, U256};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use pezkuwi_subxt_metadata::Metadata;
|
||||
|
||||
/// Default set of commonly used types by Substrate runtimes.
|
||||
// Note: We only use this at the type level, so it should be impossible to
|
||||
// create an instance of it.
|
||||
// The trait implementations exist just to make life easier,
|
||||
// but shouldn't strictly be necessary since users can't instantiate this type.
|
||||
#[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
|
||||
pub enum SubstrateConfig {}
|
||||
|
||||
impl Config for SubstrateConfig {
|
||||
type AccountId = AccountId32;
|
||||
type Address = MultiAddress<Self::AccountId, u32>;
|
||||
type Signature = MultiSignature;
|
||||
type Hasher = DynamicHasher256;
|
||||
type Header = SubstrateHeader<u32, DynamicHasher256>;
|
||||
type ExtrinsicParams = SubstrateExtrinsicParams<Self>;
|
||||
type AssetId = u32;
|
||||
}
|
||||
|
||||
/// A struct representing the signed extra and additional parameters required
|
||||
/// to construct a transaction for the default substrate node.
|
||||
pub type SubstrateExtrinsicParams<T> = DefaultExtrinsicParams<T>;
|
||||
|
||||
/// A builder which leads to [`SubstrateExtrinsicParams`] being constructed.
|
||||
/// This is what you provide to methods like `sign_and_submit()`.
|
||||
pub type SubstrateExtrinsicParamsBuilder<T> = DefaultExtrinsicParamsBuilder<T>;
|
||||
|
||||
/// A hasher (ie implements [`Hasher`]) which hashes values using the blaks2_256 algorithm.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct BlakeTwo256;
|
||||
|
||||
impl Hasher for BlakeTwo256 {
|
||||
type Output = H256;
|
||||
|
||||
fn new(_metadata: &Metadata) -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
fn hash(&self, s: &[u8]) -> Self::Output {
|
||||
pezsp_crypto_hashing::blake2_256(s).into()
|
||||
}
|
||||
}
|
||||
|
||||
/// A hasher (ie implements [`Hasher`]) which inspects the runtime metadata to decide how to
|
||||
/// hash types, falling back to blake2_256 if the hasher information is not available.
|
||||
///
|
||||
/// Currently this hasher supports only `BlakeTwo256` and `Keccak256` hashing methods.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct DynamicHasher256(HashType);
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum HashType {
|
||||
// Most chains use this:
|
||||
BlakeTwo256,
|
||||
// Chains like Hyperbridge use this (tends to be eth compatible chains)
|
||||
Keccak256,
|
||||
// If we don't have V16 metadata, we'll emit this and default to BlakeTwo256.
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl Hasher for DynamicHasher256 {
|
||||
type Output = H256;
|
||||
|
||||
fn new(metadata: &Metadata) -> Self {
|
||||
// Determine the Hash associated type used for the current chain, if possible.
|
||||
let Some(system_pallet) = metadata.pallet_by_name("System") else {
|
||||
return Self(HashType::Unknown);
|
||||
};
|
||||
let Some(hash_ty_id) = system_pallet.associated_type_id("Hashing") else {
|
||||
return Self(HashType::Unknown);
|
||||
};
|
||||
|
||||
let ty = metadata
|
||||
.types()
|
||||
.resolve(hash_ty_id)
|
||||
.expect("Type information for 'Hashing' associated type should be in metadata");
|
||||
|
||||
let hash_type = match ty.path.ident().as_deref().unwrap_or("") {
|
||||
"BlakeTwo256" => HashType::BlakeTwo256,
|
||||
"Keccak256" => HashType::Keccak256,
|
||||
_ => HashType::Unknown,
|
||||
};
|
||||
|
||||
Self(hash_type)
|
||||
}
|
||||
|
||||
fn hash(&self, s: &[u8]) -> Self::Output {
|
||||
match self.0 {
|
||||
HashType::BlakeTwo256 | HashType::Unknown => pezsp_crypto_hashing::blake2_256(s).into(),
|
||||
HashType::Keccak256 => pezsp_crypto_hashing::keccak_256(s).into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A generic Substrate header type, adapted from `sp_runtime::generic::Header`.
|
||||
/// The block number and hasher can be configured to adapt this for other nodes.
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SubstrateHeader<N: Copy + Into<U256> + TryFrom<U256>, H: Hasher> {
|
||||
/// The parent hash.
|
||||
pub parent_hash: H::Output,
|
||||
/// The block number.
|
||||
#[serde(
|
||||
serialize_with = "serialize_number",
|
||||
deserialize_with = "deserialize_number"
|
||||
)]
|
||||
#[codec(compact)]
|
||||
pub number: N,
|
||||
/// The state trie merkle root
|
||||
pub state_root: H::Output,
|
||||
/// The merkle root of the extrinsics.
|
||||
pub extrinsics_root: H::Output,
|
||||
/// A chain-specific digest of data useful for light clients or referencing auxiliary data.
|
||||
pub digest: Digest,
|
||||
}
|
||||
|
||||
impl<N, H> Header for SubstrateHeader<N, H>
|
||||
where
|
||||
N: Copy + Into<u64> + Into<U256> + TryFrom<U256> + Encode,
|
||||
H: Hasher,
|
||||
SubstrateHeader<N, H>: Encode + Decode,
|
||||
{
|
||||
type Number = N;
|
||||
type Hasher = H;
|
||||
|
||||
fn number(&self) -> Self::Number {
|
||||
self.number
|
||||
}
|
||||
}
|
||||
|
||||
/// Generic header digest. From `sp_runtime::generic::digest`.
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct Digest {
|
||||
/// A list of digest items.
|
||||
pub logs: Vec<DigestItem>,
|
||||
}
|
||||
|
||||
/// Digest item that is able to encode/decode 'system' digest items and
|
||||
/// provide opaque access to other items. From `sp_runtime::generic::digest`.
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub enum DigestItem {
|
||||
/// A pre-runtime digest.
|
||||
///
|
||||
/// These are messages from the consensus engine to the runtime, although
|
||||
/// the consensus engine can (and should) read them itself to avoid
|
||||
/// code and state duplication. It is erroneous for a runtime to produce
|
||||
/// these, but this is not (yet) checked.
|
||||
///
|
||||
/// NOTE: the runtime is not allowed to panic or fail in an `on_initialize`
|
||||
/// call if an expected `PreRuntime` digest is not present. It is the
|
||||
/// responsibility of a external block verifier to check this. Runtime API calls
|
||||
/// will initialize the block without pre-runtime digests, so initialization
|
||||
/// cannot fail when they are missing.
|
||||
PreRuntime(ConsensusEngineId, Vec<u8>),
|
||||
|
||||
/// A message from the runtime to the consensus engine. This should *never*
|
||||
/// be generated by the native code of any consensus engine, but this is not
|
||||
/// checked (yet).
|
||||
Consensus(ConsensusEngineId, Vec<u8>),
|
||||
|
||||
/// Put a Seal on it. This is only used by native code, and is never seen
|
||||
/// by runtimes.
|
||||
Seal(ConsensusEngineId, Vec<u8>),
|
||||
|
||||
/// Some other thing. Unsupported and experimental.
|
||||
Other(Vec<u8>),
|
||||
|
||||
/// An indication for the light clients that the runtime execution
|
||||
/// environment is updated.
|
||||
///
|
||||
/// Currently this is triggered when:
|
||||
/// 1. Runtime code blob is changed or
|
||||
/// 2. `heap_pages` value is changed.
|
||||
RuntimeEnvironmentUpdated,
|
||||
}
|
||||
|
||||
// From sp_runtime::generic, DigestItem enum indexes are encoded using this:
|
||||
#[repr(u32)]
|
||||
#[derive(Encode, Decode)]
|
||||
enum DigestItemType {
|
||||
Other = 0u32,
|
||||
Consensus = 4u32,
|
||||
Seal = 5u32,
|
||||
PreRuntime = 6u32,
|
||||
RuntimeEnvironmentUpdated = 8u32,
|
||||
}
|
||||
impl Encode for DigestItem {
|
||||
fn encode(&self) -> Vec<u8> {
|
||||
let mut v = Vec::new();
|
||||
|
||||
match self {
|
||||
Self::Consensus(val, data) => {
|
||||
DigestItemType::Consensus.encode_to(&mut v);
|
||||
(val, data).encode_to(&mut v);
|
||||
}
|
||||
Self::Seal(val, sig) => {
|
||||
DigestItemType::Seal.encode_to(&mut v);
|
||||
(val, sig).encode_to(&mut v);
|
||||
}
|
||||
Self::PreRuntime(val, data) => {
|
||||
DigestItemType::PreRuntime.encode_to(&mut v);
|
||||
(val, data).encode_to(&mut v);
|
||||
}
|
||||
Self::Other(val) => {
|
||||
DigestItemType::Other.encode_to(&mut v);
|
||||
val.encode_to(&mut v);
|
||||
}
|
||||
Self::RuntimeEnvironmentUpdated => {
|
||||
DigestItemType::RuntimeEnvironmentUpdated.encode_to(&mut v);
|
||||
}
|
||||
}
|
||||
|
||||
v
|
||||
}
|
||||
}
|
||||
impl Decode for DigestItem {
|
||||
fn decode<I: codec::Input>(input: &mut I) -> Result<Self, codec::Error> {
|
||||
let item_type: DigestItemType = Decode::decode(input)?;
|
||||
match item_type {
|
||||
DigestItemType::PreRuntime => {
|
||||
let vals: (ConsensusEngineId, Vec<u8>) = Decode::decode(input)?;
|
||||
Ok(Self::PreRuntime(vals.0, vals.1))
|
||||
}
|
||||
DigestItemType::Consensus => {
|
||||
let vals: (ConsensusEngineId, Vec<u8>) = Decode::decode(input)?;
|
||||
Ok(Self::Consensus(vals.0, vals.1))
|
||||
}
|
||||
DigestItemType::Seal => {
|
||||
let vals: (ConsensusEngineId, Vec<u8>) = Decode::decode(input)?;
|
||||
Ok(Self::Seal(vals.0, vals.1))
|
||||
}
|
||||
DigestItemType::Other => Ok(Self::Other(Decode::decode(input)?)),
|
||||
DigestItemType::RuntimeEnvironmentUpdated => Ok(Self::RuntimeEnvironmentUpdated),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Consensus engine unique ID. From `sp_runtime::ConsensusEngineId`.
|
||||
pub type ConsensusEngineId = [u8; 4];
|
||||
|
||||
impl serde::Serialize for DigestItem {
|
||||
fn serialize<S>(&self, seq: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
self.using_encoded(|bytes| impl_serde::serialize::serialize(bytes, seq))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> serde::Deserialize<'a> for DigestItem {
|
||||
fn deserialize<D>(de: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'a>,
|
||||
{
|
||||
let r = impl_serde::serialize::deserialize(de)?;
|
||||
Decode::decode(&mut &r[..])
|
||||
.map_err(|e| serde::de::Error::custom(format!("Decode error: {e}")))
|
||||
}
|
||||
}
|
||||
|
||||
fn serialize_number<S, T: Copy + Into<U256>>(val: &T, s: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
let u256: U256 = (*val).into();
|
||||
serde::Serialize::serialize(&u256, s)
|
||||
}
|
||||
|
||||
fn deserialize_number<'a, D, T: TryFrom<U256>>(d: D) -> Result<T, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'a>,
|
||||
{
|
||||
// At the time of writing, Smoldot gives back block numbers in numeric rather
|
||||
// than hex format. So let's support deserializing from both here:
|
||||
let number_or_hex = NumberOrHex::deserialize(d)?;
|
||||
let u256 = number_or_hex.into_u256();
|
||||
TryFrom::try_from(u256).map_err(|_| serde::de::Error::custom("Try from failed"))
|
||||
}
|
||||
|
||||
/// A number type that can be serialized both as a number or a string that encodes a number in a
|
||||
/// string.
|
||||
///
|
||||
/// We allow two representations of the block number as input. Either we deserialize to the type
|
||||
/// that is specified in the block type or we attempt to parse given hex value.
|
||||
///
|
||||
/// The primary motivation for having this type is to avoid overflows when using big integers in
|
||||
/// JavaScript (which we consider as an important RPC API consumer).
|
||||
#[derive(Copy, Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
|
||||
#[serde(untagged)]
|
||||
pub enum NumberOrHex {
|
||||
/// The number represented directly.
|
||||
Number(u64),
|
||||
/// Hex representation of the number.
|
||||
Hex(U256),
|
||||
}
|
||||
|
||||
impl NumberOrHex {
|
||||
/// Converts this number into an U256.
|
||||
pub fn into_u256(self) -> U256 {
|
||||
match self {
|
||||
NumberOrHex::Number(n) => n.into(),
|
||||
NumberOrHex::Hex(h) => h,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<NumberOrHex> for U256 {
|
||||
fn from(num_or_hex: NumberOrHex) -> U256 {
|
||||
num_or_hex.into_u256()
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! into_number_or_hex {
|
||||
($($t: ty)+) => {
|
||||
$(
|
||||
impl From<$t> for NumberOrHex {
|
||||
fn from(x: $t) -> Self {
|
||||
NumberOrHex::Number(x.into())
|
||||
}
|
||||
}
|
||||
)+
|
||||
}
|
||||
}
|
||||
into_number_or_hex!(u8 u16 u32 u64);
|
||||
|
||||
impl From<u128> for NumberOrHex {
|
||||
fn from(n: u128) -> Self {
|
||||
NumberOrHex::Hex(n.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<U256> for NumberOrHex {
|
||||
fn from(n: U256) -> Self {
|
||||
NumberOrHex::Hex(n)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
// Smoldot returns numeric block numbers in the header at the time of writing;
|
||||
// ensure we can deserialize them properly.
|
||||
#[test]
|
||||
fn can_deserialize_numeric_block_number() {
|
||||
let numeric_block_number_json = r#"
|
||||
{
|
||||
"digest": {
|
||||
"logs": []
|
||||
},
|
||||
"extrinsicsRoot": "0x0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"number": 4,
|
||||
"parentHash": "0xcb2690b2c85ceab55be03fc7f7f5f3857e7efeb7a020600ebd4331e10be2f7a5",
|
||||
"stateRoot": "0x0000000000000000000000000000000000000000000000000000000000000000"
|
||||
}
|
||||
"#;
|
||||
|
||||
let header: SubstrateHeader<u32, BlakeTwo256> =
|
||||
serde_json::from_str(numeric_block_number_json).expect("valid block header");
|
||||
assert_eq!(header.number(), 4);
|
||||
}
|
||||
|
||||
// Substrate returns hex block numbers; ensure we can also deserialize those OK.
|
||||
#[test]
|
||||
fn can_deserialize_hex_block_number() {
|
||||
let numeric_block_number_json = r#"
|
||||
{
|
||||
"digest": {
|
||||
"logs": []
|
||||
},
|
||||
"extrinsicsRoot": "0x0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"number": "0x04",
|
||||
"parentHash": "0xcb2690b2c85ceab55be03fc7f7f5f3857e7efeb7a020600ebd4331e10be2f7a5",
|
||||
"stateRoot": "0x0000000000000000000000000000000000000000000000000000000000000000"
|
||||
}
|
||||
"#;
|
||||
|
||||
let header: SubstrateHeader<u32, BlakeTwo256> =
|
||||
serde_json::from_str(numeric_block_number_json).expect("valid block header");
|
||||
assert_eq!(header.number(), 4);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,707 @@
|
||||
// Copyright 2019-2024 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! This module contains implementations for common transaction extensions, each
|
||||
//! of which implements [`TransactionExtension`], and can be used in conjunction with
|
||||
//! [`AnyOf`] to configure the set of transaction extensions which are known about
|
||||
//! when interacting with a chain.
|
||||
|
||||
use super::extrinsic_params::ExtrinsicParams;
|
||||
use crate::client::ClientState;
|
||||
use crate::config::ExtrinsicParamsEncoder;
|
||||
use crate::config::{Config, HashFor};
|
||||
use crate::error::ExtrinsicParamsError;
|
||||
use crate::utils::{Era, Static};
|
||||
use alloc::borrow::ToOwned;
|
||||
use alloc::boxed::Box;
|
||||
use alloc::vec::Vec;
|
||||
use codec::{Compact, Encode};
|
||||
use core::any::Any;
|
||||
use core::fmt::Debug;
|
||||
use derive_where::derive_where;
|
||||
use hashbrown::HashMap;
|
||||
use scale_decode::DecodeAsType;
|
||||
use scale_info::PortableRegistry;
|
||||
|
||||
// Re-export this here; it's a bit generically named to be re-exported from ::config.
|
||||
pub use super::extrinsic_params::Params;
|
||||
|
||||
/// A single [`TransactionExtension`] has a unique name, but is otherwise the
|
||||
/// same as [`ExtrinsicParams`] in describing how to encode the extra and
|
||||
/// additional data.
|
||||
pub trait TransactionExtension<T: Config>: ExtrinsicParams<T> {
|
||||
/// The type representing the `extra` / value bytes of a transaction extension.
|
||||
/// Decoding from this type should be symmetrical to the respective
|
||||
/// `ExtrinsicParamsEncoder::encode_value_to()` implementation of this transaction extension.
|
||||
type Decoded: DecodeAsType;
|
||||
|
||||
/// This should return true if the transaction extension matches the details given.
|
||||
/// Often, this will involve just checking that the identifier given matches that of the
|
||||
/// extension in question.
|
||||
fn matches(identifier: &str, _type_id: u32, _types: &PortableRegistry) -> bool;
|
||||
}
|
||||
|
||||
/// The [`VerifySignature`] extension. For V5 General transactions, this is how a signature
|
||||
/// is provided. The signature is constructed by signing a payload which contains the
|
||||
/// transaction call data as well as the encoded "additional" bytes for any extensions _after_
|
||||
/// this one in the list.
|
||||
pub struct VerifySignature<T: Config>(VerifySignatureDetails<T>);
|
||||
|
||||
impl<T: Config> ExtrinsicParams<T> for VerifySignature<T> {
|
||||
type Params = ();
|
||||
|
||||
fn new(_client: &ClientState<T>, _params: Self::Params) -> Result<Self, ExtrinsicParamsError> {
|
||||
Ok(VerifySignature(VerifySignatureDetails::Disabled))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> ExtrinsicParamsEncoder for VerifySignature<T> {
|
||||
fn encode_value_to(&self, v: &mut Vec<u8>) {
|
||||
self.0.encode_to(v);
|
||||
}
|
||||
fn encode_signer_payload_value_to(&self, v: &mut Vec<u8>) {
|
||||
// This extension is never encoded to the signer payload, and extensions
|
||||
// prior to this are ignored when creating said payload, so clear anything
|
||||
// we've seen so far.
|
||||
v.clear();
|
||||
}
|
||||
fn encode_implicit_to(&self, v: &mut Vec<u8>) {
|
||||
// We only use the "implicit" data for extensions _after_ this one
|
||||
// in the pipeline to form the signer payload. Thus, clear anything
|
||||
// we've seen so far.
|
||||
v.clear();
|
||||
}
|
||||
|
||||
fn inject_signature(&mut self, account: &dyn Any, signature: &dyn Any) {
|
||||
// Downcast refs back to concrete types (we use `&dyn Any`` so that the trait remains object safe)
|
||||
let account = account
|
||||
.downcast_ref::<T::AccountId>()
|
||||
.expect("A T::AccountId should have been provided")
|
||||
.clone();
|
||||
let signature = signature
|
||||
.downcast_ref::<T::Signature>()
|
||||
.expect("A T::Signature should have been provided")
|
||||
.clone();
|
||||
|
||||
// The signature is not set through params, only here, once given by a user:
|
||||
self.0 = VerifySignatureDetails::Signed { signature, account }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> TransactionExtension<T> for VerifySignature<T> {
|
||||
type Decoded = Static<VerifySignatureDetails<T>>;
|
||||
fn matches(identifier: &str, _type_id: u32, _types: &PortableRegistry) -> bool {
|
||||
identifier == "VerifySignature"
|
||||
}
|
||||
}
|
||||
|
||||
/// This allows a signature to be provided to the [`VerifySignature`] transaction extension.
|
||||
// Dev note: this must encode identically to https://github.com/paritytech/polkadot-sdk/blob/fd72d58313c297a10600037ce1bb88ec958d722e/substrate/frame/verify-signature/src/extension.rs#L43
|
||||
#[derive(codec::Encode, codec::Decode)]
|
||||
pub enum VerifySignatureDetails<T: Config> {
|
||||
/// A signature has been provided.
|
||||
Signed {
|
||||
/// The signature.
|
||||
signature: T::Signature,
|
||||
/// The account that generated the signature.
|
||||
account: T::AccountId,
|
||||
},
|
||||
/// No signature was provided.
|
||||
Disabled,
|
||||
}
|
||||
|
||||
/// The [`CheckMetadataHash`] transaction extension.
|
||||
pub struct CheckMetadataHash {
|
||||
// Eventually we might provide or calculate the metadata hash here,
|
||||
// but for now we never provide a hash and so this is empty.
|
||||
}
|
||||
|
||||
impl<T: Config> ExtrinsicParams<T> for CheckMetadataHash {
|
||||
type Params = ();
|
||||
|
||||
fn new(_client: &ClientState<T>, _params: Self::Params) -> Result<Self, ExtrinsicParamsError> {
|
||||
Ok(CheckMetadataHash {})
|
||||
}
|
||||
}
|
||||
|
||||
impl ExtrinsicParamsEncoder for CheckMetadataHash {
|
||||
fn encode_value_to(&self, v: &mut Vec<u8>) {
|
||||
// A single 0 byte in the TX payload indicates that the chain should
|
||||
// _not_ expect any metadata hash to exist in the signer payload.
|
||||
0u8.encode_to(v);
|
||||
}
|
||||
fn encode_implicit_to(&self, v: &mut Vec<u8>) {
|
||||
// We provide no metadata hash in the signer payload to align with the above.
|
||||
None::<()>.encode_to(v);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> TransactionExtension<T> for CheckMetadataHash {
|
||||
type Decoded = CheckMetadataHashMode;
|
||||
fn matches(identifier: &str, _type_id: u32, _types: &PortableRegistry) -> bool {
|
||||
identifier == "CheckMetadataHash"
|
||||
}
|
||||
}
|
||||
|
||||
/// Is metadata checking enabled or disabled?
|
||||
// Dev note: The "Disabled" and "Enabled" variant names match those that the
|
||||
// transaction extension will be encoded with, in order that DecodeAsType will work
|
||||
// properly.
|
||||
#[derive(Copy, Clone, Debug, DecodeAsType)]
|
||||
pub enum CheckMetadataHashMode {
|
||||
/// No hash was provided in the signer payload.
|
||||
Disabled,
|
||||
/// A hash was provided in the signer payload.
|
||||
Enabled,
|
||||
}
|
||||
|
||||
impl CheckMetadataHashMode {
|
||||
/// Is metadata checking enabled or disabled for this transaction?
|
||||
pub fn is_enabled(&self) -> bool {
|
||||
match self {
|
||||
CheckMetadataHashMode::Disabled => false,
|
||||
CheckMetadataHashMode::Enabled => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The [`CheckSpecVersion`] transaction extension.
|
||||
pub struct CheckSpecVersion(u32);
|
||||
|
||||
impl<T: Config> ExtrinsicParams<T> for CheckSpecVersion {
|
||||
type Params = ();
|
||||
|
||||
fn new(client: &ClientState<T>, _params: Self::Params) -> Result<Self, ExtrinsicParamsError> {
|
||||
Ok(CheckSpecVersion(client.runtime_version.spec_version))
|
||||
}
|
||||
}
|
||||
|
||||
impl ExtrinsicParamsEncoder for CheckSpecVersion {
|
||||
fn encode_implicit_to(&self, v: &mut Vec<u8>) {
|
||||
self.0.encode_to(v);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> TransactionExtension<T> for CheckSpecVersion {
|
||||
type Decoded = ();
|
||||
fn matches(identifier: &str, _type_id: u32, _types: &PortableRegistry) -> bool {
|
||||
identifier == "CheckSpecVersion"
|
||||
}
|
||||
}
|
||||
|
||||
/// The [`CheckNonce`] transaction extension.
|
||||
pub struct CheckNonce(u64);
|
||||
|
||||
impl<T: Config> ExtrinsicParams<T> for CheckNonce {
|
||||
type Params = CheckNonceParams;
|
||||
|
||||
fn new(_client: &ClientState<T>, params: Self::Params) -> Result<Self, ExtrinsicParamsError> {
|
||||
Ok(CheckNonce(params.0.unwrap_or(0)))
|
||||
}
|
||||
}
|
||||
|
||||
impl ExtrinsicParamsEncoder for CheckNonce {
|
||||
fn encode_value_to(&self, v: &mut Vec<u8>) {
|
||||
Compact(self.0).encode_to(v);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> TransactionExtension<T> for CheckNonce {
|
||||
type Decoded = u64;
|
||||
fn matches(identifier: &str, _type_id: u32, _types: &PortableRegistry) -> bool {
|
||||
identifier == "CheckNonce"
|
||||
}
|
||||
}
|
||||
|
||||
/// Configure the nonce used.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct CheckNonceParams(Option<u64>);
|
||||
|
||||
impl CheckNonceParams {
|
||||
/// Retrieve the nonce from the chain and use that.
|
||||
pub fn from_chain() -> Self {
|
||||
Self(None)
|
||||
}
|
||||
/// Manually set an account nonce to use.
|
||||
pub fn with_nonce(nonce: u64) -> Self {
|
||||
Self(Some(nonce))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> Params<T> for CheckNonceParams {
|
||||
fn inject_account_nonce(&mut self, nonce: u64) {
|
||||
if self.0.is_none() {
|
||||
self.0 = Some(nonce)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The [`CheckTxVersion`] transaction extension.
|
||||
pub struct CheckTxVersion(u32);
|
||||
|
||||
impl<T: Config> ExtrinsicParams<T> for CheckTxVersion {
|
||||
type Params = ();
|
||||
|
||||
fn new(client: &ClientState<T>, _params: Self::Params) -> Result<Self, ExtrinsicParamsError> {
|
||||
Ok(CheckTxVersion(client.runtime_version.transaction_version))
|
||||
}
|
||||
}
|
||||
|
||||
impl ExtrinsicParamsEncoder for CheckTxVersion {
|
||||
fn encode_implicit_to(&self, v: &mut Vec<u8>) {
|
||||
self.0.encode_to(v);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> TransactionExtension<T> for CheckTxVersion {
|
||||
type Decoded = ();
|
||||
fn matches(identifier: &str, _type_id: u32, _types: &PortableRegistry) -> bool {
|
||||
identifier == "CheckTxVersion"
|
||||
}
|
||||
}
|
||||
|
||||
/// The [`CheckGenesis`] transaction extension.
|
||||
pub struct CheckGenesis<T: Config>(HashFor<T>);
|
||||
|
||||
impl<T: Config> ExtrinsicParams<T> for CheckGenesis<T> {
|
||||
type Params = ();
|
||||
|
||||
fn new(client: &ClientState<T>, _params: Self::Params) -> Result<Self, ExtrinsicParamsError> {
|
||||
Ok(CheckGenesis(client.genesis_hash))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> ExtrinsicParamsEncoder for CheckGenesis<T> {
|
||||
fn encode_implicit_to(&self, v: &mut Vec<u8>) {
|
||||
self.0.encode_to(v);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> TransactionExtension<T> for CheckGenesis<T> {
|
||||
type Decoded = ();
|
||||
fn matches(identifier: &str, _type_id: u32, _types: &PortableRegistry) -> bool {
|
||||
identifier == "CheckGenesis"
|
||||
}
|
||||
}
|
||||
|
||||
/// The [`CheckMortality`] transaction extension.
|
||||
pub struct CheckMortality<T: Config> {
|
||||
params: CheckMortalityParamsInner<T>,
|
||||
genesis_hash: HashFor<T>,
|
||||
}
|
||||
|
||||
impl<T: Config> ExtrinsicParams<T> for CheckMortality<T> {
|
||||
type Params = CheckMortalityParams<T>;
|
||||
|
||||
fn new(client: &ClientState<T>, params: Self::Params) -> Result<Self, ExtrinsicParamsError> {
|
||||
// If a user has explicitly configured the transaction to be mortal for n blocks, but we get
|
||||
// to this stage and no injected information was able to turn this into MortalFromBlock{..},
|
||||
// then we hit an error as we are unable to construct a mortal transaction here.
|
||||
if matches!(¶ms.0, CheckMortalityParamsInner::MortalForBlocks(_)) {
|
||||
return Err(ExtrinsicParamsError::custom(
|
||||
"CheckMortality: We cannot construct an offline extrinsic with only the number of blocks it is mortal for. Use mortal_from_unchecked instead.",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(CheckMortality {
|
||||
// if nothing has been explicitly configured, we will have a mortal transaction
|
||||
// valid for 32 blocks if block info is available.
|
||||
params: params.0,
|
||||
genesis_hash: client.genesis_hash,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> ExtrinsicParamsEncoder for CheckMortality<T> {
|
||||
fn encode_value_to(&self, v: &mut Vec<u8>) {
|
||||
match &self.params {
|
||||
CheckMortalityParamsInner::MortalFromBlock {
|
||||
for_n_blocks,
|
||||
from_block_n,
|
||||
..
|
||||
} => {
|
||||
Era::mortal(*for_n_blocks, *from_block_n).encode_to(v);
|
||||
}
|
||||
_ => {
|
||||
// Note: if we see `CheckMortalityInner::MortalForBlocks`, then it means the user has
|
||||
// configured a block to be mortal for N blocks, but the current block was never injected,
|
||||
// so we don't know where to start from and default back to building an immortal tx.
|
||||
Era::Immortal.encode_to(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
fn encode_implicit_to(&self, v: &mut Vec<u8>) {
|
||||
match &self.params {
|
||||
CheckMortalityParamsInner::MortalFromBlock {
|
||||
from_block_hash, ..
|
||||
} => {
|
||||
from_block_hash.encode_to(v);
|
||||
}
|
||||
_ => {
|
||||
self.genesis_hash.encode_to(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> TransactionExtension<T> for CheckMortality<T> {
|
||||
type Decoded = Era;
|
||||
fn matches(identifier: &str, _type_id: u32, _types: &PortableRegistry) -> bool {
|
||||
identifier == "CheckMortality"
|
||||
}
|
||||
}
|
||||
|
||||
/// Parameters to configure the [`CheckMortality`] transaction extension.
|
||||
pub struct CheckMortalityParams<T: Config>(CheckMortalityParamsInner<T>);
|
||||
|
||||
enum CheckMortalityParamsInner<T: Config> {
|
||||
/// The transaction will be immortal.
|
||||
Immortal,
|
||||
/// The transaction is mortal for N blocks. This must be "upgraded" into
|
||||
/// [`CheckMortalityParamsInner::MortalFromBlock`] to ultimately work.
|
||||
MortalForBlocks(u64),
|
||||
/// The transaction is mortal for N blocks, but if it cannot be "upgraded",
|
||||
/// then it will be set to immortal instead. This is the default if unset.
|
||||
MortalForBlocksOrImmortalIfNotPossible(u64),
|
||||
/// The transaction is mortal and all of the relevant information is provided.
|
||||
MortalFromBlock {
|
||||
for_n_blocks: u64,
|
||||
from_block_n: u64,
|
||||
from_block_hash: HashFor<T>,
|
||||
},
|
||||
}
|
||||
|
||||
impl<T: Config> Default for CheckMortalityParams<T> {
|
||||
fn default() -> Self {
|
||||
// default to being mortal for 32 blocks if possible, else immortal:
|
||||
CheckMortalityParams(CheckMortalityParamsInner::MortalForBlocksOrImmortalIfNotPossible(32))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> CheckMortalityParams<T> {
|
||||
/// Configure a transaction that will be mortal for the number of blocks given.
|
||||
pub fn mortal(for_n_blocks: u64) -> Self {
|
||||
Self(CheckMortalityParamsInner::MortalForBlocks(for_n_blocks))
|
||||
}
|
||||
|
||||
/// Configure a transaction that will be mortal for the number of blocks given,
|
||||
/// and from the block details provided. Prefer to use [`CheckMortalityParams::mortal()`]
|
||||
/// where possible, which prevents the block number and hash from being misaligned.
|
||||
pub fn mortal_from_unchecked(
|
||||
for_n_blocks: u64,
|
||||
from_block_n: u64,
|
||||
from_block_hash: HashFor<T>,
|
||||
) -> Self {
|
||||
Self(CheckMortalityParamsInner::MortalFromBlock {
|
||||
for_n_blocks,
|
||||
from_block_n,
|
||||
from_block_hash,
|
||||
})
|
||||
}
|
||||
/// An immortal transaction.
|
||||
pub fn immortal() -> Self {
|
||||
Self(CheckMortalityParamsInner::Immortal)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> Params<T> for CheckMortalityParams<T> {
|
||||
fn inject_block(&mut self, from_block_n: u64, from_block_hash: HashFor<T>) {
|
||||
match &self.0 {
|
||||
CheckMortalityParamsInner::MortalForBlocks(n)
|
||||
| CheckMortalityParamsInner::MortalForBlocksOrImmortalIfNotPossible(n) => {
|
||||
self.0 = CheckMortalityParamsInner::MortalFromBlock {
|
||||
for_n_blocks: *n,
|
||||
from_block_n,
|
||||
from_block_hash,
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// Don't change anything if explicit Immortal or explicit block set.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The [`ChargeAssetTxPayment`] transaction extension.
|
||||
#[derive(DecodeAsType)]
|
||||
#[derive_where(Clone, Debug; T::AssetId)]
|
||||
#[decode_as_type(trait_bounds = "T::AssetId: DecodeAsType")]
|
||||
pub struct ChargeAssetTxPayment<T: Config> {
|
||||
tip: Compact<u128>,
|
||||
asset_id: Option<T::AssetId>,
|
||||
}
|
||||
|
||||
impl<T: Config> ChargeAssetTxPayment<T> {
|
||||
/// Tip to the extrinsic author in the native chain token.
|
||||
pub fn tip(&self) -> u128 {
|
||||
self.tip.0
|
||||
}
|
||||
|
||||
/// Tip to the extrinsic author using the asset ID given.
|
||||
pub fn asset_id(&self) -> Option<&T::AssetId> {
|
||||
self.asset_id.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> ExtrinsicParams<T> for ChargeAssetTxPayment<T> {
|
||||
type Params = ChargeAssetTxPaymentParams<T>;
|
||||
|
||||
fn new(_client: &ClientState<T>, params: Self::Params) -> Result<Self, ExtrinsicParamsError> {
|
||||
Ok(ChargeAssetTxPayment {
|
||||
tip: Compact(params.tip),
|
||||
asset_id: params.asset_id,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> ExtrinsicParamsEncoder for ChargeAssetTxPayment<T> {
|
||||
fn encode_value_to(&self, v: &mut Vec<u8>) {
|
||||
(self.tip, &self.asset_id).encode_to(v);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> TransactionExtension<T> for ChargeAssetTxPayment<T> {
|
||||
type Decoded = Self;
|
||||
fn matches(identifier: &str, _type_id: u32, _types: &PortableRegistry) -> bool {
|
||||
identifier == "ChargeAssetTxPayment"
|
||||
}
|
||||
}
|
||||
|
||||
/// Parameters to configure the [`ChargeAssetTxPayment`] transaction extension.
|
||||
pub struct ChargeAssetTxPaymentParams<T: Config> {
|
||||
tip: u128,
|
||||
asset_id: Option<T::AssetId>,
|
||||
}
|
||||
|
||||
impl<T: Config> Default for ChargeAssetTxPaymentParams<T> {
|
||||
fn default() -> Self {
|
||||
ChargeAssetTxPaymentParams {
|
||||
tip: Default::default(),
|
||||
asset_id: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> ChargeAssetTxPaymentParams<T> {
|
||||
/// Don't provide a tip to the extrinsic author.
|
||||
pub fn no_tip() -> Self {
|
||||
ChargeAssetTxPaymentParams {
|
||||
tip: 0,
|
||||
asset_id: None,
|
||||
}
|
||||
}
|
||||
/// Tip the extrinsic author in the native chain token.
|
||||
pub fn tip(tip: u128) -> Self {
|
||||
ChargeAssetTxPaymentParams {
|
||||
tip,
|
||||
asset_id: None,
|
||||
}
|
||||
}
|
||||
/// Tip the extrinsic author using the asset ID given.
|
||||
pub fn tip_of(tip: u128, asset_id: T::AssetId) -> Self {
|
||||
ChargeAssetTxPaymentParams {
|
||||
tip,
|
||||
asset_id: Some(asset_id),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> Params<T> for ChargeAssetTxPaymentParams<T> {}
|
||||
|
||||
/// The [`ChargeTransactionPayment`] transaction extension.
|
||||
#[derive(Clone, Debug, DecodeAsType)]
|
||||
pub struct ChargeTransactionPayment {
|
||||
tip: Compact<u128>,
|
||||
}
|
||||
|
||||
impl ChargeTransactionPayment {
|
||||
/// Tip to the extrinsic author in the native chain token.
|
||||
pub fn tip(&self) -> u128 {
|
||||
self.tip.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> ExtrinsicParams<T> for ChargeTransactionPayment {
|
||||
type Params = ChargeTransactionPaymentParams;
|
||||
|
||||
fn new(_client: &ClientState<T>, params: Self::Params) -> Result<Self, ExtrinsicParamsError> {
|
||||
Ok(ChargeTransactionPayment {
|
||||
tip: Compact(params.tip),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ExtrinsicParamsEncoder for ChargeTransactionPayment {
|
||||
fn encode_value_to(&self, v: &mut Vec<u8>) {
|
||||
self.tip.encode_to(v);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> TransactionExtension<T> for ChargeTransactionPayment {
|
||||
type Decoded = Self;
|
||||
fn matches(identifier: &str, _type_id: u32, _types: &PortableRegistry) -> bool {
|
||||
identifier == "ChargeTransactionPayment"
|
||||
}
|
||||
}
|
||||
|
||||
/// Parameters to configure the [`ChargeTransactionPayment`] transaction extension.
|
||||
#[derive(Default)]
|
||||
pub struct ChargeTransactionPaymentParams {
|
||||
tip: u128,
|
||||
}
|
||||
|
||||
impl ChargeTransactionPaymentParams {
|
||||
/// Don't provide a tip to the extrinsic author.
|
||||
pub fn no_tip() -> Self {
|
||||
ChargeTransactionPaymentParams { tip: 0 }
|
||||
}
|
||||
/// Tip the extrinsic author in the native chain token.
|
||||
pub fn tip(tip: u128) -> Self {
|
||||
ChargeTransactionPaymentParams { tip }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> Params<T> for ChargeTransactionPaymentParams {}
|
||||
|
||||
/// This accepts a tuple of [`TransactionExtension`]s, and will dynamically make use of whichever
|
||||
/// ones are actually required for the chain in the correct order, ignoring the rest. This
|
||||
/// is a sensible default, and allows for a single configuration to work across multiple chains.
|
||||
pub struct AnyOf<T, Params> {
|
||||
params: Vec<Box<dyn ExtrinsicParamsEncoder + Send + 'static>>,
|
||||
_marker: core::marker::PhantomData<(T, Params)>,
|
||||
}
|
||||
|
||||
macro_rules! impl_tuples {
|
||||
($($ident:ident $index:tt),+) => {
|
||||
// We do some magic when the tuple is wrapped in AnyOf. We
|
||||
// look at the metadata, and use this to select and make use of only the extensions
|
||||
// that we actually need for the chain we're dealing with.
|
||||
impl <T, $($ident),+> ExtrinsicParams<T> for AnyOf<T, ($($ident,)+)>
|
||||
where
|
||||
T: Config,
|
||||
$($ident: TransactionExtension<T>,)+
|
||||
{
|
||||
type Params = ($($ident::Params,)+);
|
||||
|
||||
fn new(
|
||||
client: &ClientState<T>,
|
||||
params: Self::Params,
|
||||
) -> Result<Self, ExtrinsicParamsError> {
|
||||
let metadata = &client.metadata;
|
||||
let types = metadata.types();
|
||||
|
||||
// For each transaction extension in the tuple, find the matching index in the metadata, if
|
||||
// there is one, and add it to a map with that index as the key.
|
||||
let mut exts_by_index = HashMap::new();
|
||||
$({
|
||||
for (idx, e) in metadata.extrinsic().transaction_extensions_to_use_for_encoding().enumerate() {
|
||||
// Skip over any exts that have a match already:
|
||||
if exts_by_index.contains_key(&idx) {
|
||||
continue
|
||||
}
|
||||
// Break and record as soon as we find a match:
|
||||
if $ident::matches(e.identifier(), e.extra_ty(), types) {
|
||||
let ext = $ident::new(client, params.$index)?;
|
||||
let boxed_ext: Box<dyn ExtrinsicParamsEncoder + Send + 'static> = Box::new(ext);
|
||||
exts_by_index.insert(idx, boxed_ext);
|
||||
break
|
||||
}
|
||||
}
|
||||
})+
|
||||
|
||||
// Next, turn these into an ordered vec, erroring if we haven't matched on any exts yet.
|
||||
let mut params = Vec::new();
|
||||
for (idx, e) in metadata.extrinsic().transaction_extensions_to_use_for_encoding().enumerate() {
|
||||
let Some(ext) = exts_by_index.remove(&idx) else {
|
||||
if is_type_empty(e.extra_ty(), types) {
|
||||
continue
|
||||
} else {
|
||||
return Err(ExtrinsicParamsError::UnknownTransactionExtension(e.identifier().to_owned()));
|
||||
}
|
||||
};
|
||||
params.push(ext);
|
||||
}
|
||||
|
||||
Ok(AnyOf {
|
||||
params,
|
||||
_marker: core::marker::PhantomData
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl <T, $($ident),+> ExtrinsicParamsEncoder for AnyOf<T, ($($ident,)+)>
|
||||
where
|
||||
T: Config,
|
||||
$($ident: TransactionExtension<T>,)+
|
||||
{
|
||||
fn encode_value_to(&self, v: &mut Vec<u8>) {
|
||||
for ext in &self.params {
|
||||
ext.encode_value_to(v);
|
||||
}
|
||||
}
|
||||
fn encode_signer_payload_value_to(&self, v: &mut Vec<u8>) {
|
||||
for ext in &self.params {
|
||||
ext.encode_signer_payload_value_to(v);
|
||||
}
|
||||
}
|
||||
fn encode_implicit_to(&self, v: &mut Vec<u8>) {
|
||||
for ext in &self.params {
|
||||
ext.encode_implicit_to(v);
|
||||
}
|
||||
}
|
||||
fn inject_signature(&mut self, account_id: &dyn Any, signature: &dyn Any) {
|
||||
for ext in &mut self.params {
|
||||
ext.inject_signature(account_id, signature);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[rustfmt::skip]
|
||||
const _: () = {
|
||||
impl_tuples!(A 0);
|
||||
impl_tuples!(A 0, B 1);
|
||||
impl_tuples!(A 0, B 1, C 2);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3, E 4);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13, O 14);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13, O 14, P 15);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13, O 14, P 15, Q 16);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13, O 14, P 15, Q 16, R 17);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13, O 14, P 15, Q 16, R 17, S 18);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13, O 14, P 15, Q 16, R 17, S 18, U 19);
|
||||
impl_tuples!(A 0, B 1, C 2, D 3, E 4, F 5, G 6, H 7, I 8, J 9, K 10, L 11, M 12, N 13, O 14, P 15, Q 16, R 17, S 18, U 19, V 20);
|
||||
};
|
||||
|
||||
/// Checks to see whether the type being given is empty, ie would require
|
||||
/// 0 bytes to encode.
|
||||
fn is_type_empty(type_id: u32, types: &scale_info::PortableRegistry) -> bool {
|
||||
let Some(ty) = types.resolve(type_id) else {
|
||||
// Can't resolve; type may not be empty. Not expected to hit this.
|
||||
return false;
|
||||
};
|
||||
|
||||
use scale_info::TypeDef;
|
||||
match &ty.type_def {
|
||||
TypeDef::Composite(c) => c.fields.iter().all(|f| is_type_empty(f.ty.id, types)),
|
||||
TypeDef::Array(a) => a.len == 0 || is_type_empty(a.type_param.id, types),
|
||||
TypeDef::Tuple(t) => t.fields.iter().all(|f| is_type_empty(f.id, types)),
|
||||
// Explicitly list these in case any additions are made in the future.
|
||||
TypeDef::BitSequence(_)
|
||||
| TypeDef::Variant(_)
|
||||
| TypeDef::Sequence(_)
|
||||
| TypeDef::Compact(_)
|
||||
| TypeDef::Primitive(_) => false,
|
||||
}
|
||||
}
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
// Copyright 2019-2024 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! Construct addresses to access constants with.
|
||||
|
||||
use alloc::borrow::Cow;
|
||||
use alloc::string::String;
|
||||
use derive_where::derive_where;
|
||||
use scale_decode::DecodeAsType;
|
||||
|
||||
/// This represents a constant address. Anything implementing this trait
|
||||
/// can be used to fetch constants.
|
||||
pub trait Address {
|
||||
/// The target type of the value that lives at this address.
|
||||
type Target: DecodeAsType;
|
||||
|
||||
/// The name of the pallet that the constant lives under.
|
||||
fn pallet_name(&self) -> &str;
|
||||
|
||||
/// The name of the constant in a given pallet.
|
||||
fn constant_name(&self) -> &str;
|
||||
|
||||
/// An optional hash which, if present, will be checked against
|
||||
/// the node metadata to confirm that the return type matches what
|
||||
/// we are expecting.
|
||||
fn validation_hash(&self) -> Option<[u8; 32]> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
// Any reference to an address is a valid address.
|
||||
impl<A: Address + ?Sized> Address for &'_ A {
|
||||
type Target = A::Target;
|
||||
|
||||
fn pallet_name(&self) -> &str {
|
||||
A::pallet_name(*self)
|
||||
}
|
||||
|
||||
fn constant_name(&self) -> &str {
|
||||
A::constant_name(*self)
|
||||
}
|
||||
|
||||
fn validation_hash(&self) -> Option<[u8; 32]> {
|
||||
A::validation_hash(*self)
|
||||
}
|
||||
}
|
||||
|
||||
// (str, str) and similar are valid addresses.
|
||||
impl<A: AsRef<str>, B: AsRef<str>> Address for (A, B) {
|
||||
type Target = scale_value::Value;
|
||||
|
||||
fn pallet_name(&self) -> &str {
|
||||
self.0.as_ref()
|
||||
}
|
||||
|
||||
fn constant_name(&self) -> &str {
|
||||
self.1.as_ref()
|
||||
}
|
||||
|
||||
fn validation_hash(&self) -> Option<[u8; 32]> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// This represents the address of a constant.
|
||||
#[derive_where(Clone, Debug, PartialOrd, Ord, PartialEq, Eq)]
|
||||
pub struct StaticAddress<ReturnTy> {
|
||||
pallet_name: Cow<'static, str>,
|
||||
constant_name: Cow<'static, str>,
|
||||
constant_hash: Option<[u8; 32]>,
|
||||
_marker: core::marker::PhantomData<ReturnTy>,
|
||||
}
|
||||
|
||||
/// A dynamic lookup address to access a constant.
|
||||
pub type DynamicAddress<ReturnTy> = StaticAddress<ReturnTy>;
|
||||
|
||||
impl<ReturnTy> StaticAddress<ReturnTy> {
|
||||
/// Create a new [`StaticAddress`] to use to look up a constant.
|
||||
pub fn new(pallet_name: impl Into<String>, constant_name: impl Into<String>) -> Self {
|
||||
Self {
|
||||
pallet_name: Cow::Owned(pallet_name.into()),
|
||||
constant_name: Cow::Owned(constant_name.into()),
|
||||
constant_hash: None,
|
||||
_marker: core::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new [`StaticAddress`] that will be validated
|
||||
/// against node metadata using the hash given.
|
||||
#[doc(hidden)]
|
||||
pub fn new_static(
|
||||
pallet_name: &'static str,
|
||||
constant_name: &'static str,
|
||||
hash: [u8; 32],
|
||||
) -> Self {
|
||||
Self {
|
||||
pallet_name: Cow::Borrowed(pallet_name),
|
||||
constant_name: Cow::Borrowed(constant_name),
|
||||
constant_hash: Some(hash),
|
||||
_marker: core::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Do not validate this constant prior to accessing it.
|
||||
pub fn unvalidated(self) -> Self {
|
||||
Self {
|
||||
pallet_name: self.pallet_name,
|
||||
constant_name: self.constant_name,
|
||||
constant_hash: None,
|
||||
_marker: self._marker,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<ReturnTy: DecodeAsType> Address for StaticAddress<ReturnTy> {
|
||||
type Target = ReturnTy;
|
||||
|
||||
fn pallet_name(&self) -> &str {
|
||||
&self.pallet_name
|
||||
}
|
||||
|
||||
fn constant_name(&self) -> &str {
|
||||
&self.constant_name
|
||||
}
|
||||
|
||||
fn validation_hash(&self) -> Option<[u8; 32]> {
|
||||
self.constant_hash
|
||||
}
|
||||
}
|
||||
|
||||
/// Construct a new dynamic constant lookup.
|
||||
pub fn dynamic<ReturnTy: DecodeAsType>(
|
||||
pallet_name: impl Into<String>,
|
||||
constant_name: impl Into<String>,
|
||||
) -> DynamicAddress<ReturnTy> {
|
||||
DynamicAddress::new(pallet_name, constant_name)
|
||||
}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
// Copyright 2019-2024 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! Access constants from metadata.
|
||||
//!
|
||||
//! Use [`get`] to retrieve a constant from some metadata, or [`validate`] to check that a static
|
||||
//! constant address lines up with the value seen in the metadata.
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! ```rust
|
||||
//! use pezkuwi_subxt_macro::subxt;
|
||||
//! use pezkuwi_subxt_core::constants;
|
||||
//! use pezkuwi_subxt_core::Metadata;
|
||||
//!
|
||||
//! // If we generate types without `subxt`, we need to point to `::pezkuwi_subxt_core`:
|
||||
//! #[subxt(
|
||||
//! crate = "::pezkuwi_subxt_core",
|
||||
//! runtime_metadata_path = "../artifacts/polkadot_metadata_small.scale",
|
||||
//! )]
|
||||
//! pub mod polkadot {}
|
||||
//!
|
||||
//! // Some metadata we'd like to access constants in:
|
||||
//! let metadata_bytes = include_bytes!("../../../artifacts/polkadot_metadata_small.scale");
|
||||
//! let metadata = Metadata::decode_from(&metadata_bytes[..]).unwrap();
|
||||
//!
|
||||
//! // We can use a static address to obtain some constant:
|
||||
//! let address = polkadot::constants().balances().existential_deposit();
|
||||
//!
|
||||
//! // This validates that the address given is in line with the metadata
|
||||
//! // we're trying to access the constant in:
|
||||
//! constants::validate(&address, &metadata).expect("is valid");
|
||||
//!
|
||||
//! // This acquires the constant (and internally also validates it):
|
||||
//! let ed = constants::get(&address, &metadata).expect("can decode constant");
|
||||
//!
|
||||
//! assert_eq!(ed, 33_333_333);
|
||||
//! ```
|
||||
|
||||
pub mod address;
|
||||
|
||||
use crate::Metadata;
|
||||
use crate::error::ConstantError;
|
||||
use address::Address;
|
||||
use alloc::borrow::ToOwned;
|
||||
use alloc::string::ToString;
|
||||
use alloc::vec::Vec;
|
||||
use frame_decode::constants::ConstantTypeInfo;
|
||||
use scale_decode::IntoVisitor;
|
||||
|
||||
/// When the provided `address` is statically generated via the `#[subxt]` macro, this validates
|
||||
/// that the shape of the constant value is the same as the shape expected by the static address.
|
||||
///
|
||||
/// When the provided `address` is dynamic (and thus does not come with any expectation of the
|
||||
/// shape of the constant value), this just returns `Ok(())`
|
||||
pub fn validate<Addr: Address>(address: Addr, metadata: &Metadata) -> Result<(), ConstantError> {
|
||||
if let Some(actual_hash) = address.validation_hash() {
|
||||
let expected_hash = metadata
|
||||
.pallet_by_name(address.pallet_name())
|
||||
.ok_or_else(|| ConstantError::PalletNameNotFound(address.pallet_name().to_string()))?
|
||||
.constant_hash(address.constant_name())
|
||||
.ok_or_else(|| ConstantError::ConstantNameNotFound {
|
||||
pallet_name: address.pallet_name().to_string(),
|
||||
constant_name: address.constant_name().to_owned(),
|
||||
})?;
|
||||
if actual_hash != expected_hash {
|
||||
return Err(ConstantError::IncompatibleCodegen);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Fetch a constant out of the metadata given a constant address. If the `address` has been
|
||||
/// statically generated, this will validate that the constant shape is as expected, too.
|
||||
pub fn get<Addr: Address>(
|
||||
address: Addr,
|
||||
metadata: &Metadata,
|
||||
) -> Result<Addr::Target, ConstantError> {
|
||||
// 1. Validate constant shape if hash given:
|
||||
validate(&address, metadata)?;
|
||||
|
||||
// 2. Attempt to decode the constant into the type given:
|
||||
let constant = frame_decode::constants::decode_constant(
|
||||
address.pallet_name(),
|
||||
address.constant_name(),
|
||||
metadata,
|
||||
metadata.types(),
|
||||
Addr::Target::into_visitor(),
|
||||
)
|
||||
.map_err(ConstantError::CouldNotDecodeConstant)?;
|
||||
|
||||
Ok(constant)
|
||||
}
|
||||
|
||||
/// Access the bytes of a constant by the address it is registered under.
|
||||
pub fn get_bytes<Addr: Address>(
|
||||
address: Addr,
|
||||
metadata: &Metadata,
|
||||
) -> Result<Vec<u8>, ConstantError> {
|
||||
// 1. Validate custom value shape if hash given:
|
||||
validate(&address, metadata)?;
|
||||
|
||||
// 2. Return the underlying bytes:
|
||||
let constant = metadata
|
||||
.constant_info(address.pallet_name(), address.constant_name())
|
||||
.map_err(|e| ConstantError::ConstantInfoError(e.into_owned()))?;
|
||||
Ok(constant.bytes.to_vec())
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
// Copyright 2019-2024 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! Construct addresses to access custom values with.
|
||||
|
||||
use alloc::borrow::Cow;
|
||||
use alloc::string::String;
|
||||
use derive_where::derive_where;
|
||||
use scale_decode::DecodeAsType;
|
||||
|
||||
/// Use this with [`Address::IsDecodable`].
|
||||
pub use crate::utils::{Maybe, No, NoMaybe};
|
||||
|
||||
/// This represents the address of a custom value in the metadata.
|
||||
/// Anything that implements it can be used to fetch custom values from the metadata.
|
||||
/// The trait is implemented by [`str`] for dynamic lookup and [`StaticAddress`] for static queries.
|
||||
pub trait Address {
|
||||
/// The type of the custom value.
|
||||
type Target: DecodeAsType;
|
||||
/// Should be set to `Yes` for Dynamic values and static values that have a valid type.
|
||||
/// Should be `No` for custom values, that have an invalid type id.
|
||||
type IsDecodable: NoMaybe;
|
||||
|
||||
/// the name (key) by which the custom value can be accessed in the metadata.
|
||||
fn name(&self) -> &str;
|
||||
|
||||
/// An optional hash which, if present, can be checked against node metadata.
|
||||
fn validation_hash(&self) -> Option<[u8; 32]> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
// Any reference to an address is a valid address
|
||||
impl<A: Address + ?Sized> Address for &'_ A {
|
||||
type Target = A::Target;
|
||||
type IsDecodable = A::IsDecodable;
|
||||
|
||||
fn name(&self) -> &str {
|
||||
A::name(*self)
|
||||
}
|
||||
|
||||
fn validation_hash(&self) -> Option<[u8; 32]> {
|
||||
A::validation_hash(*self)
|
||||
}
|
||||
}
|
||||
|
||||
// Support plain strings for looking up custom values.
|
||||
impl Address for str {
|
||||
type Target = scale_value::Value;
|
||||
type IsDecodable = Maybe;
|
||||
|
||||
fn name(&self) -> &str {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// A static address to a custom value.
|
||||
#[derive_where(Clone, Debug, PartialOrd, Ord, PartialEq, Eq)]
|
||||
pub struct StaticAddress<ReturnTy, IsDecodable> {
|
||||
name: Cow<'static, str>,
|
||||
hash: Option<[u8; 32]>,
|
||||
marker: core::marker::PhantomData<(ReturnTy, IsDecodable)>,
|
||||
}
|
||||
|
||||
/// A dynamic address to a custom value.
|
||||
pub type DynamicAddress<ReturnTy> = StaticAddress<ReturnTy, Maybe>;
|
||||
|
||||
impl<ReturnTy, IsDecodable> StaticAddress<ReturnTy, IsDecodable> {
|
||||
#[doc(hidden)]
|
||||
/// Creates a new StaticAddress.
|
||||
pub fn new_static(name: &'static str, hash: [u8; 32]) -> Self {
|
||||
Self {
|
||||
name: Cow::Borrowed(name),
|
||||
hash: Some(hash),
|
||||
marker: core::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new [`StaticAddress`]
|
||||
pub fn new(name: impl Into<String>) -> Self {
|
||||
Self {
|
||||
name: name.into().into(),
|
||||
hash: None,
|
||||
marker: core::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Do not validate this custom value prior to accessing it.
|
||||
pub fn unvalidated(self) -> Self {
|
||||
Self {
|
||||
name: self.name,
|
||||
hash: None,
|
||||
marker: self.marker,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Target: DecodeAsType, IsDecodable: NoMaybe> Address for StaticAddress<Target, IsDecodable> {
|
||||
type Target = Target;
|
||||
type IsDecodable = IsDecodable;
|
||||
|
||||
fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
fn validation_hash(&self) -> Option<[u8; 32]> {
|
||||
self.hash
|
||||
}
|
||||
}
|
||||
|
||||
/// Construct a new dynamic custom value lookup.
|
||||
pub fn dynamic<ReturnTy: DecodeAsType>(
|
||||
custom_value_name: impl Into<String>,
|
||||
) -> DynamicAddress<ReturnTy> {
|
||||
DynamicAddress::new(custom_value_name)
|
||||
}
|
||||
+179
@@ -0,0 +1,179 @@
|
||||
// Copyright 2019-2024 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! Access custom values from metadata.
|
||||
//!
|
||||
//! Use [`get`] to retrieve a custom value from some metadata, or [`validate`] to check that a
|
||||
//! static custom value address lines up with the value seen in the metadata.
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! ```rust
|
||||
//! use pezkuwi_subxt_macro::subxt;
|
||||
//! use pezkuwi_subxt_core::custom_values;
|
||||
//! use pezkuwi_subxt_core::Metadata;
|
||||
//!
|
||||
//! // If we generate types without `subxt`, we need to point to `::pezkuwi_subxt_core`:
|
||||
//! #[subxt(
|
||||
//! crate = "::pezkuwi_subxt_core",
|
||||
//! runtime_metadata_path = "../artifacts/polkadot_metadata_small.scale",
|
||||
//! )]
|
||||
//! pub mod polkadot {}
|
||||
//!
|
||||
//! // Some metadata we'd like to access custom values in:
|
||||
//! let metadata_bytes = include_bytes!("../../../artifacts/polkadot_metadata_small.scale");
|
||||
//! let metadata = Metadata::decode_from(&metadata_bytes[..]).unwrap();
|
||||
//!
|
||||
//! // At the moment, we don't expect to see any custom values in the metadata
|
||||
//! // for Polkadot, so this will return an error:
|
||||
//! let err = custom_values::get("Foo", &metadata);
|
||||
//! ```
|
||||
|
||||
pub mod address;
|
||||
|
||||
use crate::utils::Maybe;
|
||||
use crate::{Metadata, error::CustomValueError};
|
||||
use address::Address;
|
||||
use alloc::vec::Vec;
|
||||
use frame_decode::custom_values::CustomValueTypeInfo;
|
||||
use scale_decode::IntoVisitor;
|
||||
|
||||
/// Run the validation logic against some custom value address you'd like to access. Returns `Ok(())`
|
||||
/// if the address is valid (or if it's not possible to check since the address has no validation hash).
|
||||
/// Returns an error if the address was not valid (wrong name, type or raw bytes)
|
||||
pub fn validate<Addr: Address>(address: Addr, metadata: &Metadata) -> Result<(), CustomValueError> {
|
||||
if let Some(actual_hash) = address.validation_hash() {
|
||||
let custom = metadata.custom();
|
||||
let custom_value = custom
|
||||
.get(address.name())
|
||||
.ok_or_else(|| CustomValueError::NotFound(address.name().into()))?;
|
||||
let expected_hash = custom_value.hash();
|
||||
if actual_hash != expected_hash {
|
||||
return Err(CustomValueError::IncompatibleCodegen);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Access a custom value by the address it is registered under. This can be just a [str] to get back a dynamic value,
|
||||
/// or a static address from the generated static interface to get a value of a static type returned.
|
||||
pub fn get<Addr: Address<IsDecodable = Maybe>>(
|
||||
address: Addr,
|
||||
metadata: &Metadata,
|
||||
) -> Result<Addr::Target, CustomValueError> {
|
||||
// 1. Validate custom value shape if hash given:
|
||||
validate(&address, metadata)?;
|
||||
|
||||
// 2. Attempt to decode custom value:
|
||||
let value = frame_decode::custom_values::decode_custom_value(
|
||||
address.name(),
|
||||
metadata,
|
||||
metadata.types(),
|
||||
Addr::Target::into_visitor(),
|
||||
)
|
||||
.map_err(CustomValueError::CouldNotDecodeCustomValue)?;
|
||||
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
/// Access the bytes of a custom value by the address it is registered under.
|
||||
pub fn get_bytes<Addr: Address>(
|
||||
address: Addr,
|
||||
metadata: &Metadata,
|
||||
) -> Result<Vec<u8>, CustomValueError> {
|
||||
// 1. Validate custom value shape if hash given:
|
||||
validate(&address, metadata)?;
|
||||
|
||||
// 2. Return the underlying bytes:
|
||||
let custom_value = metadata
|
||||
.custom_value_info(address.name())
|
||||
.map_err(|e| CustomValueError::NotFound(e.not_found))?;
|
||||
Ok(custom_value.bytes.to_vec())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use alloc::collections::BTreeMap;
|
||||
use codec::Encode;
|
||||
use scale_decode::DecodeAsType;
|
||||
use scale_info::TypeInfo;
|
||||
use scale_info::form::PortableForm;
|
||||
|
||||
use alloc::borrow::ToOwned;
|
||||
use alloc::string::String;
|
||||
use alloc::vec;
|
||||
|
||||
use crate::custom_values;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Encode, TypeInfo, DecodeAsType)]
|
||||
pub struct Person {
|
||||
age: u16,
|
||||
name: String,
|
||||
}
|
||||
|
||||
fn mock_metadata() -> Metadata {
|
||||
let person_ty = scale_info::MetaType::new::<Person>();
|
||||
let unit = scale_info::MetaType::new::<()>();
|
||||
let mut types = scale_info::Registry::new();
|
||||
let person_ty_id = types.register_type(&person_ty);
|
||||
let unit_id = types.register_type(&unit);
|
||||
let types: scale_info::PortableRegistry = types.into();
|
||||
|
||||
let person = Person {
|
||||
age: 42,
|
||||
name: "Neo".into(),
|
||||
};
|
||||
|
||||
let person_value_metadata: frame_metadata::v15::CustomValueMetadata<PortableForm> =
|
||||
frame_metadata::v15::CustomValueMetadata {
|
||||
ty: person_ty_id,
|
||||
value: person.encode(),
|
||||
};
|
||||
|
||||
let frame_metadata = frame_metadata::v15::RuntimeMetadataV15 {
|
||||
types,
|
||||
pallets: vec![],
|
||||
extrinsic: frame_metadata::v15::ExtrinsicMetadata {
|
||||
version: 0,
|
||||
address_ty: unit_id,
|
||||
call_ty: unit_id,
|
||||
signature_ty: unit_id,
|
||||
extra_ty: unit_id,
|
||||
signed_extensions: vec![],
|
||||
},
|
||||
ty: unit_id,
|
||||
apis: vec![],
|
||||
outer_enums: frame_metadata::v15::OuterEnums {
|
||||
call_enum_ty: unit_id,
|
||||
event_enum_ty: unit_id,
|
||||
error_enum_ty: unit_id,
|
||||
},
|
||||
custom: frame_metadata::v15::CustomMetadata {
|
||||
map: BTreeMap::from_iter([("Mr. Robot".to_owned(), person_value_metadata)]),
|
||||
},
|
||||
};
|
||||
|
||||
let metadata: pezkuwi_subxt_metadata::Metadata = frame_metadata.try_into().unwrap();
|
||||
metadata
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decoding() {
|
||||
let metadata = mock_metadata();
|
||||
|
||||
assert!(custom_values::get("Invalid Address", &metadata).is_err());
|
||||
|
||||
let person_addr = custom_values::address::dynamic::<Person>("Mr. Robot");
|
||||
let person = custom_values::get(&person_addr, &metadata).unwrap();
|
||||
assert_eq!(
|
||||
person,
|
||||
Person {
|
||||
age: 42,
|
||||
name: "Neo".into()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
// Copyright 2019-2024 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! This module provides the entry points to create dynamic
|
||||
//! transactions, storage and constant lookups.
|
||||
|
||||
pub use scale_value::{At, Value};
|
||||
|
||||
// Submit dynamic transactions.
|
||||
pub use crate::tx::payload::dynamic as tx;
|
||||
|
||||
// Lookup constants dynamically.
|
||||
pub use crate::constants::address::dynamic as constant;
|
||||
|
||||
// Lookup storage values dynamically.
|
||||
pub use crate::storage::address::dynamic as storage;
|
||||
|
||||
// Execute runtime API function call dynamically.
|
||||
pub use crate::runtime_api::payload::dynamic as runtime_api_call;
|
||||
|
||||
// Execute View Function API function call dynamically.
|
||||
pub use crate::view_functions::payload::dynamic as view_function_call;
|
||||
|
||||
/// Obtain a custom value from the metadata.
|
||||
pub use crate::custom_values::address::dynamic as custom_value;
|
||||
+319
@@ -0,0 +1,319 @@
|
||||
// Copyright 2019-2024 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! The errors that can be emitted in this crate.
|
||||
|
||||
use alloc::boxed::Box;
|
||||
use alloc::string::String;
|
||||
use alloc::vec::Vec;
|
||||
use thiserror::Error as DeriveError;
|
||||
|
||||
/// The error emitted when something goes wrong.
|
||||
#[derive(Debug, DeriveError)]
|
||||
#[allow(missing_docs)]
|
||||
pub enum Error {
|
||||
#[error(transparent)]
|
||||
StorageError(#[from] StorageError),
|
||||
#[error(transparent)]
|
||||
Extrinsic(#[from] ExtrinsicError),
|
||||
#[error(transparent)]
|
||||
Constant(#[from] ConstantError),
|
||||
#[error(transparent)]
|
||||
CustomValue(#[from] CustomValueError),
|
||||
#[error(transparent)]
|
||||
RuntimeApi(#[from] RuntimeApiError),
|
||||
#[error(transparent)]
|
||||
ViewFunction(#[from] ViewFunctionError),
|
||||
#[error(transparent)]
|
||||
Events(#[from] EventsError),
|
||||
}
|
||||
|
||||
#[derive(Debug, DeriveError)]
|
||||
#[non_exhaustive]
|
||||
#[allow(missing_docs)]
|
||||
pub enum EventsError {
|
||||
#[error("Can't decode event: can't decode phase: {0}")]
|
||||
CannotDecodePhase(codec::Error),
|
||||
#[error("Can't decode event: can't decode pallet index: {0}")]
|
||||
CannotDecodePalletIndex(codec::Error),
|
||||
#[error("Can't decode event: can't decode variant index: {0}")]
|
||||
CannotDecodeVariantIndex(codec::Error),
|
||||
#[error("Can't decode event: can't find pallet with index {0}")]
|
||||
CannotFindPalletWithIndex(u8),
|
||||
#[error(
|
||||
"Can't decode event: can't find variant with index {variant_index} in pallet {pallet_name}"
|
||||
)]
|
||||
CannotFindVariantWithIndex {
|
||||
pallet_name: String,
|
||||
variant_index: u8,
|
||||
},
|
||||
#[error("Can't decode field {field_name:?} in event {pallet_name}.{event_name}: {reason}")]
|
||||
CannotDecodeFieldInEvent {
|
||||
pallet_name: String,
|
||||
event_name: String,
|
||||
field_name: String,
|
||||
reason: scale_decode::visitor::DecodeError,
|
||||
},
|
||||
#[error("Can't decode event topics: {0}")]
|
||||
CannotDecodeEventTopics(codec::Error),
|
||||
#[error("Can't decode the fields of event {pallet_name}.{event_name}: {reason}")]
|
||||
CannotDecodeEventFields {
|
||||
pallet_name: String,
|
||||
event_name: String,
|
||||
reason: scale_decode::Error,
|
||||
},
|
||||
#[error("Can't decode event {pallet_name}.{event_name} to Event enum: {reason}")]
|
||||
CannotDecodeEventEnum {
|
||||
pallet_name: String,
|
||||
event_name: String,
|
||||
reason: scale_decode::Error,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, DeriveError)]
|
||||
#[non_exhaustive]
|
||||
#[allow(missing_docs)]
|
||||
pub enum ViewFunctionError {
|
||||
#[error("The static View Function address used is not compatible with the live chain")]
|
||||
IncompatibleCodegen,
|
||||
#[error("Can't find View Function: pallet {0} not found")]
|
||||
PalletNotFound(String),
|
||||
#[error("Can't find View Function {function_name} in pallet {pallet_name}")]
|
||||
ViewFunctionNotFound {
|
||||
pallet_name: String,
|
||||
function_name: String,
|
||||
},
|
||||
#[error("Failed to encode View Function inputs: {0}")]
|
||||
CouldNotEncodeInputs(frame_decode::view_functions::ViewFunctionInputsEncodeError),
|
||||
#[error("Failed to decode View Function: {0}")]
|
||||
CouldNotDecodeResponse(frame_decode::view_functions::ViewFunctionDecodeError<u32>),
|
||||
}
|
||||
|
||||
#[derive(Debug, DeriveError)]
|
||||
#[non_exhaustive]
|
||||
#[allow(missing_docs)]
|
||||
pub enum RuntimeApiError {
|
||||
#[error("The static Runtime API address used is not compatible with the live chain")]
|
||||
IncompatibleCodegen,
|
||||
#[error("Runtime API trait not found: {0}")]
|
||||
TraitNotFound(String),
|
||||
#[error("Runtime API method {method_name} not found in trait {trait_name}")]
|
||||
MethodNotFound {
|
||||
trait_name: String,
|
||||
method_name: String,
|
||||
},
|
||||
#[error("Failed to encode Runtime API inputs: {0}")]
|
||||
CouldNotEncodeInputs(frame_decode::runtime_apis::RuntimeApiInputsEncodeError),
|
||||
#[error("Failed to decode Runtime API: {0}")]
|
||||
CouldNotDecodeResponse(frame_decode::runtime_apis::RuntimeApiDecodeError<u32>),
|
||||
}
|
||||
|
||||
#[derive(Debug, DeriveError)]
|
||||
#[non_exhaustive]
|
||||
#[allow(missing_docs)]
|
||||
pub enum CustomValueError {
|
||||
#[error("The static custom value address used is not compatible with the live chain")]
|
||||
IncompatibleCodegen,
|
||||
#[error("The custom value '{0}' was not found")]
|
||||
NotFound(String),
|
||||
#[error("Failed to decode custom value: {0}")]
|
||||
CouldNotDecodeCustomValue(frame_decode::custom_values::CustomValueDecodeError<u32>),
|
||||
}
|
||||
|
||||
/// Something went wrong working with a constant.
|
||||
#[derive(Debug, DeriveError)]
|
||||
#[non_exhaustive]
|
||||
#[allow(missing_docs)]
|
||||
pub enum ConstantError {
|
||||
#[error("The static constant address used is not compatible with the live chain")]
|
||||
IncompatibleCodegen,
|
||||
#[error("Can't find constant: pallet with name {0} not found")]
|
||||
PalletNameNotFound(String),
|
||||
#[error(
|
||||
"Constant '{constant_name}' not found in pallet {pallet_name} in the live chain metadata"
|
||||
)]
|
||||
ConstantNameNotFound {
|
||||
pallet_name: String,
|
||||
constant_name: String,
|
||||
},
|
||||
#[error("Failed to decode constant: {0}")]
|
||||
CouldNotDecodeConstant(frame_decode::constants::ConstantDecodeError<u32>),
|
||||
#[error("Cannot obtain constant information from metadata: {0}")]
|
||||
ConstantInfoError(frame_decode::constants::ConstantInfoError<'static>),
|
||||
}
|
||||
|
||||
/// Something went wrong trying to encode or decode a storage address.
|
||||
#[derive(Debug, DeriveError)]
|
||||
#[non_exhaustive]
|
||||
#[allow(missing_docs)]
|
||||
pub enum StorageError {
|
||||
#[error("The static storage address used is not compatible with the live chain")]
|
||||
IncompatibleCodegen,
|
||||
#[error("Can't find storage value: pallet with name {0} not found")]
|
||||
PalletNameNotFound(String),
|
||||
#[error(
|
||||
"Storage entry '{entry_name}' not found in pallet {pallet_name} in the live chain metadata"
|
||||
)]
|
||||
StorageEntryNotFound {
|
||||
pallet_name: String,
|
||||
entry_name: String,
|
||||
},
|
||||
#[error("Cannot obtain storage information from metadata: {0}")]
|
||||
StorageInfoError(frame_decode::storage::StorageInfoError<'static>),
|
||||
#[error("Cannot encode storage key: {0}")]
|
||||
StorageKeyEncodeError(frame_decode::storage::StorageKeyEncodeError),
|
||||
#[error("Cannot create a key to iterate over a plain entry")]
|
||||
CannotIterPlainEntry {
|
||||
pallet_name: String,
|
||||
entry_name: String,
|
||||
},
|
||||
#[error(
|
||||
"Wrong number of key parts provided to iterate a storage address. We expected at most {max_expected} key parts but got {got} key parts"
|
||||
)]
|
||||
WrongNumberOfKeyPartsProvidedForIterating { max_expected: usize, got: usize },
|
||||
#[error(
|
||||
"Wrong number of key parts provided to fetch a storage address. We expected {expected} key parts but got {got} key parts"
|
||||
)]
|
||||
WrongNumberOfKeyPartsProvidedForFetching { expected: usize, got: usize },
|
||||
}
|
||||
|
||||
#[derive(Debug, DeriveError)]
|
||||
#[non_exhaustive]
|
||||
#[allow(missing_docs)]
|
||||
pub enum StorageKeyError {
|
||||
#[error("Can't decode the storage key: {error}")]
|
||||
StorageKeyDecodeError {
|
||||
bytes: Vec<u8>,
|
||||
error: frame_decode::storage::StorageKeyDecodeError<u32>,
|
||||
},
|
||||
#[error("Can't decode the values from the storage key: {0}")]
|
||||
CannotDecodeValuesInKey(frame_decode::storage::StorageKeyValueDecodeError),
|
||||
#[error(
|
||||
"Cannot decode storage key: there were leftover bytes, indicating that the decoding failed"
|
||||
)]
|
||||
LeftoverBytes { bytes: Vec<u8> },
|
||||
#[error("Can't decode a single value from the storage key part at index {index}: {error}")]
|
||||
CannotDecodeValueInKey {
|
||||
index: usize,
|
||||
error: scale_decode::Error,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, DeriveError)]
|
||||
#[non_exhaustive]
|
||||
#[allow(missing_docs)]
|
||||
pub enum StorageValueError {
|
||||
#[error("Cannot decode storage value: {0}")]
|
||||
CannotDecode(frame_decode::storage::StorageValueDecodeError<u32>),
|
||||
#[error(
|
||||
"Cannot decode storage value: there were leftover bytes, indicating that the decoding failed"
|
||||
)]
|
||||
LeftoverBytes { bytes: Vec<u8> },
|
||||
}
|
||||
|
||||
/// An error that can be encountered when constructing a transaction.
|
||||
#[derive(Debug, DeriveError)]
|
||||
#[allow(missing_docs)]
|
||||
pub enum ExtrinsicError {
|
||||
#[error("The extrinsic payload is not compatible with the live chain")]
|
||||
IncompatibleCodegen,
|
||||
#[error("Can't find extrinsic: pallet with name {0} not found")]
|
||||
PalletNameNotFound(String),
|
||||
#[error("Can't find extrinsic: call name {call_name} doesn't exist in pallet {pallet_name}")]
|
||||
CallNameNotFound {
|
||||
pallet_name: String,
|
||||
call_name: String,
|
||||
},
|
||||
#[error("Can't encode the extrinsic call data: {0}")]
|
||||
CannotEncodeCallData(scale_encode::Error),
|
||||
#[error("Subxt does not support the extrinsic versions expected by the chain")]
|
||||
UnsupportedVersion,
|
||||
#[error("Cannot construct the required transaction extensions: {0}")]
|
||||
Params(#[from] ExtrinsicParamsError),
|
||||
#[error("Cannot decode transaction extension '{name}': {error}")]
|
||||
CouldNotDecodeTransactionExtension {
|
||||
/// The extension name.
|
||||
name: String,
|
||||
/// The decode error.
|
||||
error: scale_decode::Error,
|
||||
},
|
||||
#[error(
|
||||
"After decoding the extrinsic at index {extrinsic_index}, {num_leftover_bytes} bytes were left, suggesting that decoding may have failed"
|
||||
)]
|
||||
LeftoverBytes {
|
||||
/// Index of the extrinsic that failed to decode.
|
||||
extrinsic_index: usize,
|
||||
/// Number of bytes leftover after decoding the extrinsic.
|
||||
num_leftover_bytes: usize,
|
||||
},
|
||||
#[error("{0}")]
|
||||
ExtrinsicDecodeErrorAt(#[from] ExtrinsicDecodeErrorAt),
|
||||
#[error("Failed to decode the fields of an extrinsic at index {extrinsic_index}: {error}")]
|
||||
CannotDecodeFields {
|
||||
/// Index of the extrinsic whose fields we could not decode
|
||||
extrinsic_index: usize,
|
||||
/// The decode error.
|
||||
error: scale_decode::Error,
|
||||
},
|
||||
#[error("Failed to decode the extrinsic at index {extrinsic_index} to a root enum: {error}")]
|
||||
CannotDecodeIntoRootExtrinsic {
|
||||
/// Index of the extrinsic that we failed to decode
|
||||
extrinsic_index: usize,
|
||||
/// The decode error.
|
||||
error: scale_decode::Error,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[non_exhaustive]
|
||||
#[allow(missing_docs)]
|
||||
#[error("Cannot decode extrinsic at index {extrinsic_index}: {error}")]
|
||||
pub struct ExtrinsicDecodeErrorAt {
|
||||
pub extrinsic_index: usize,
|
||||
pub error: ExtrinsicDecodeErrorAtReason,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[non_exhaustive]
|
||||
#[allow(missing_docs)]
|
||||
pub enum ExtrinsicDecodeErrorAtReason {
|
||||
#[error("{0}")]
|
||||
DecodeError(frame_decode::extrinsics::ExtrinsicDecodeError),
|
||||
#[error("Leftover bytes")]
|
||||
LeftoverBytes(Vec<u8>),
|
||||
}
|
||||
|
||||
/// An error that can be emitted when trying to construct an instance of [`crate::config::ExtrinsicParams`],
|
||||
/// encode data from the instance, or match on signed extensions.
|
||||
#[derive(Debug, DeriveError)]
|
||||
#[non_exhaustive]
|
||||
#[allow(missing_docs)]
|
||||
pub enum ExtrinsicParamsError {
|
||||
#[error("Cannot find type id '{type_id} in the metadata (context: {context})")]
|
||||
MissingTypeId {
|
||||
/// Type ID.
|
||||
type_id: u32,
|
||||
/// Some arbitrary context to help narrow the source of the error.
|
||||
context: &'static str,
|
||||
},
|
||||
#[error("The chain expects a signed extension with the name {0}, but we did not provide one")]
|
||||
UnknownTransactionExtension(String),
|
||||
#[error("Error constructing extrinsic parameters: {0}")]
|
||||
Custom(Box<dyn core::error::Error + Send + Sync + 'static>),
|
||||
}
|
||||
|
||||
impl ExtrinsicParamsError {
|
||||
/// Create a custom [`ExtrinsicParamsError`] from a string.
|
||||
pub fn custom<S: Into<String>>(error: S) -> Self {
|
||||
let error: String = error.into();
|
||||
let error: Box<dyn core::error::Error + Send + Sync + 'static> = Box::from(error);
|
||||
ExtrinsicParamsError::Custom(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<core::convert::Infallible> for ExtrinsicParamsError {
|
||||
fn from(value: core::convert::Infallible) -> Self {
|
||||
match value {}
|
||||
}
|
||||
}
|
||||
+1022
File diff suppressed because it is too large
Load Diff
Vendored
+49
@@ -0,0 +1,49 @@
|
||||
// Copyright 2019-2024 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! # subxt-core
|
||||
//!
|
||||
//! A `#[no_std]` compatible subset of the functionality provided in the `subxt` crate. This
|
||||
//! contains the core logic for encoding and decoding things, but nothing related to networking.
|
||||
//!
|
||||
//! Here's an overview of the main things exposed here:
|
||||
//!
|
||||
//! - [`blocks`]: decode and explore block bodies.
|
||||
//! - [`constants`]: access and validate the constant addresses in some metadata.
|
||||
//! - [`custom_values`]: access and validate the custom value addresses in some metadata.
|
||||
//! - [`storage`]: construct storage request payloads and decode the results you'd get back.
|
||||
//! - [`tx`]: construct and sign transactions (extrinsics).
|
||||
//! - [`runtime_api`]: construct runtime API request payloads and decode the results you'd get back.
|
||||
//! - [`events`]: decode and explore events.
|
||||
//!
|
||||
|
||||
#![deny(missing_docs)]
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
pub extern crate alloc;
|
||||
|
||||
pub mod blocks;
|
||||
pub mod client;
|
||||
pub mod config;
|
||||
pub mod constants;
|
||||
pub mod custom_values;
|
||||
pub mod dynamic;
|
||||
pub mod error;
|
||||
pub mod events;
|
||||
pub mod runtime_api;
|
||||
pub mod storage;
|
||||
pub mod tx;
|
||||
pub mod utils;
|
||||
pub mod view_functions;
|
||||
|
||||
pub use config::Config;
|
||||
pub use error::Error;
|
||||
pub use pezkuwi_subxt_metadata::Metadata;
|
||||
|
||||
/// Re-exports of some of the key external crates.
|
||||
pub mod ext {
|
||||
pub use codec;
|
||||
pub use scale_decode;
|
||||
pub use scale_encode;
|
||||
pub use scale_value;
|
||||
}
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
// Copyright 2019-2024 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! Encode runtime API payloads, decode the associated values returned from them, and validate
|
||||
//! static runtime API payloads.
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! ```rust
|
||||
//! use pezkuwi_subxt_macro::subxt;
|
||||
//! use pezkuwi_subxt_core::runtime_api;
|
||||
//! use pezkuwi_subxt_core::Metadata;
|
||||
//!
|
||||
//! // If we generate types without `subxt`, we need to point to `::pezkuwi_subxt_core`:
|
||||
//! #[subxt(
|
||||
//! crate = "::pezkuwi_subxt_core",
|
||||
//! runtime_metadata_path = "../artifacts/polkadot_metadata_small.scale",
|
||||
//! )]
|
||||
//! pub mod polkadot {}
|
||||
//!
|
||||
//! // Some metadata we'll use to work with storage entries:
|
||||
//! let metadata_bytes = include_bytes!("../../../artifacts/polkadot_metadata_small.scale");
|
||||
//! let metadata = Metadata::decode_from(&metadata_bytes[..]).unwrap();
|
||||
//!
|
||||
//! // Build a storage query to access account information.
|
||||
//! let payload = polkadot::apis().metadata().metadata_versions();
|
||||
//!
|
||||
//! // We can validate that the payload is compatible with the given metadata.
|
||||
//! runtime_api::validate(&payload, &metadata).unwrap();
|
||||
//!
|
||||
//! // Encode the payload name and arguments to hand to a node:
|
||||
//! let _call_name = runtime_api::call_name(&payload);
|
||||
//! let _call_args = runtime_api::call_args(&payload, &metadata).unwrap();
|
||||
//!
|
||||
//! // If we were to obtain a value back from the node, we could
|
||||
//! // then decode it using the same payload and metadata like so:
|
||||
//! let value_bytes = hex::decode("080e0000000f000000").unwrap();
|
||||
//! let value = runtime_api::decode_value(&mut &*value_bytes, &payload, &metadata).unwrap();
|
||||
//!
|
||||
//! println!("Available metadata versions: {value:?}");
|
||||
//! ```
|
||||
|
||||
pub mod payload;
|
||||
|
||||
use crate::Metadata;
|
||||
use crate::error::RuntimeApiError;
|
||||
use alloc::format;
|
||||
use alloc::string::{String, ToString};
|
||||
use alloc::vec::Vec;
|
||||
use payload::Payload;
|
||||
use scale_decode::IntoVisitor;
|
||||
|
||||
/// Run the validation logic against some runtime API payload you'd like to use. Returns `Ok(())`
|
||||
/// if the payload is valid (or if it's not possible to check since the payload has no validation hash).
|
||||
/// Return an error if the payload was not valid or something went wrong trying to validate it (ie
|
||||
/// the runtime API in question do not exist at all)
|
||||
pub fn validate<P: Payload>(payload: P, metadata: &Metadata) -> Result<(), RuntimeApiError> {
|
||||
let Some(hash) = payload.validation_hash() else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let trait_name = payload.trait_name();
|
||||
let method_name = payload.method_name();
|
||||
|
||||
let api_trait = metadata
|
||||
.runtime_api_trait_by_name(trait_name)
|
||||
.ok_or_else(|| RuntimeApiError::TraitNotFound(trait_name.to_string()))?;
|
||||
let api_method =
|
||||
api_trait
|
||||
.method_by_name(method_name)
|
||||
.ok_or_else(|| RuntimeApiError::MethodNotFound {
|
||||
trait_name: trait_name.to_string(),
|
||||
method_name: method_name.to_string(),
|
||||
})?;
|
||||
|
||||
if hash != api_method.hash() {
|
||||
Err(RuntimeApiError::IncompatibleCodegen)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the name of the runtime API call from the payload.
|
||||
pub fn call_name<P: Payload>(payload: P) -> String {
|
||||
format!("{}_{}", payload.trait_name(), payload.method_name())
|
||||
}
|
||||
|
||||
/// Return the encoded call args given a runtime API payload.
|
||||
pub fn call_args<P: Payload>(payload: P, metadata: &Metadata) -> Result<Vec<u8>, RuntimeApiError> {
|
||||
let value = frame_decode::runtime_apis::encode_runtime_api_inputs(
|
||||
payload.trait_name(),
|
||||
payload.method_name(),
|
||||
payload.args(),
|
||||
metadata,
|
||||
metadata.types(),
|
||||
)
|
||||
.map_err(RuntimeApiError::CouldNotEncodeInputs)?;
|
||||
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
/// Decode the value bytes at the location given by the provided runtime API payload.
|
||||
pub fn decode_value<P: Payload>(
|
||||
bytes: &mut &[u8],
|
||||
payload: P,
|
||||
metadata: &Metadata,
|
||||
) -> Result<P::ReturnType, RuntimeApiError> {
|
||||
let value = frame_decode::runtime_apis::decode_runtime_api_response(
|
||||
payload.trait_name(),
|
||||
payload.method_name(),
|
||||
bytes,
|
||||
metadata,
|
||||
metadata.types(),
|
||||
P::ReturnType::into_visitor(),
|
||||
)
|
||||
.map_err(RuntimeApiError::CouldNotDecodeResponse)?;
|
||||
|
||||
Ok(value)
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
// Copyright 2019-2024 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! This module contains the trait and types used to represent
|
||||
//! runtime API calls that can be made.
|
||||
|
||||
use alloc::borrow::Cow;
|
||||
use alloc::string::String;
|
||||
use core::marker::PhantomData;
|
||||
use derive_where::derive_where;
|
||||
use frame_decode::runtime_apis::IntoEncodableValues;
|
||||
use scale_decode::DecodeAsType;
|
||||
|
||||
/// This represents a runtime API payload that can be used to call a Runtime API on
|
||||
/// a chain and decode the response.
|
||||
pub trait Payload {
|
||||
/// Type of the arguments.
|
||||
type ArgsType: IntoEncodableValues;
|
||||
/// The return type of the function call.
|
||||
type ReturnType: DecodeAsType;
|
||||
|
||||
/// The runtime API trait name.
|
||||
fn trait_name(&self) -> &str;
|
||||
|
||||
/// The runtime API method name.
|
||||
fn method_name(&self) -> &str;
|
||||
|
||||
/// The input arguments.
|
||||
fn args(&self) -> &Self::ArgsType;
|
||||
|
||||
/// Returns the statically generated validation hash.
|
||||
fn validation_hash(&self) -> Option<[u8; 32]> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
// Any reference to a payload is a valid payload.
|
||||
impl<P: Payload + ?Sized> Payload for &'_ P {
|
||||
type ArgsType = P::ArgsType;
|
||||
type ReturnType = P::ReturnType;
|
||||
|
||||
fn trait_name(&self) -> &str {
|
||||
P::trait_name(*self)
|
||||
}
|
||||
|
||||
fn method_name(&self) -> &str {
|
||||
P::method_name(*self)
|
||||
}
|
||||
|
||||
fn args(&self) -> &Self::ArgsType {
|
||||
P::args(*self)
|
||||
}
|
||||
|
||||
fn validation_hash(&self) -> Option<[u8; 32]> {
|
||||
P::validation_hash(*self)
|
||||
}
|
||||
}
|
||||
|
||||
/// A runtime API payload containing the generic argument data
|
||||
/// and interpreting the result of the call as `ReturnTy`.
|
||||
///
|
||||
/// This can be created from static values (ie those generated
|
||||
/// via the `subxt` macro) or dynamic values via [`dynamic`].
|
||||
#[derive_where(Clone, Debug, Eq, Ord, PartialEq, PartialOrd; ArgsType)]
|
||||
pub struct StaticPayload<ArgsType, ReturnType> {
|
||||
trait_name: Cow<'static, str>,
|
||||
method_name: Cow<'static, str>,
|
||||
args: ArgsType,
|
||||
validation_hash: Option<[u8; 32]>,
|
||||
_marker: PhantomData<ReturnType>,
|
||||
}
|
||||
|
||||
/// A dynamic runtime API payload.
|
||||
pub type DynamicPayload<ArgsType, ReturnType> = StaticPayload<ArgsType, ReturnType>;
|
||||
|
||||
impl<ArgsType: IntoEncodableValues, ReturnType: DecodeAsType> Payload
|
||||
for StaticPayload<ArgsType, ReturnType>
|
||||
{
|
||||
type ArgsType = ArgsType;
|
||||
type ReturnType = ReturnType;
|
||||
|
||||
fn trait_name(&self) -> &str {
|
||||
&self.trait_name
|
||||
}
|
||||
|
||||
fn method_name(&self) -> &str {
|
||||
&self.method_name
|
||||
}
|
||||
|
||||
fn args(&self) -> &Self::ArgsType {
|
||||
&self.args
|
||||
}
|
||||
|
||||
fn validation_hash(&self) -> Option<[u8; 32]> {
|
||||
self.validation_hash
|
||||
}
|
||||
}
|
||||
|
||||
impl<ArgsType, ReturnTy> StaticPayload<ArgsType, ReturnTy> {
|
||||
/// Create a new [`StaticPayload`].
|
||||
pub fn new(
|
||||
trait_name: impl Into<String>,
|
||||
method_name: impl Into<String>,
|
||||
args: ArgsType,
|
||||
) -> Self {
|
||||
StaticPayload {
|
||||
trait_name: trait_name.into().into(),
|
||||
method_name: method_name.into().into(),
|
||||
args,
|
||||
validation_hash: None,
|
||||
_marker: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new static [`StaticPayload`] using static function name
|
||||
/// and scale-encoded argument data.
|
||||
///
|
||||
/// This is only expected to be used from codegen.
|
||||
#[doc(hidden)]
|
||||
pub fn new_static(
|
||||
trait_name: &'static str,
|
||||
method_name: &'static str,
|
||||
args: ArgsType,
|
||||
hash: [u8; 32],
|
||||
) -> StaticPayload<ArgsType, ReturnTy> {
|
||||
StaticPayload {
|
||||
trait_name: Cow::Borrowed(trait_name),
|
||||
method_name: Cow::Borrowed(method_name),
|
||||
args,
|
||||
validation_hash: Some(hash),
|
||||
_marker: core::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Do not validate this call prior to submitting it.
|
||||
pub fn unvalidated(self) -> Self {
|
||||
Self {
|
||||
validation_hash: None,
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the trait name.
|
||||
pub fn trait_name(&self) -> &str {
|
||||
&self.trait_name
|
||||
}
|
||||
|
||||
/// Returns the method name.
|
||||
pub fn method_name(&self) -> &str {
|
||||
&self.method_name
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new [`DynamicPayload`].
|
||||
pub fn dynamic<ArgsType, ReturnType>(
|
||||
trait_name: impl Into<String>,
|
||||
method_name: impl Into<String>,
|
||||
args_data: ArgsType,
|
||||
) -> DynamicPayload<ArgsType, ReturnType> {
|
||||
DynamicPayload::new(trait_name, method_name, args_data)
|
||||
}
|
||||
+171
@@ -0,0 +1,171 @@
|
||||
// Copyright 2019-2024 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! Construct addresses to access storage entries with.
|
||||
|
||||
use crate::utils::{Maybe, YesMaybe};
|
||||
use alloc::borrow::Cow;
|
||||
use alloc::string::String;
|
||||
use alloc::vec::Vec;
|
||||
use frame_decode::storage::{IntoDecodableValues, IntoEncodableValues};
|
||||
use scale_decode::DecodeAsType;
|
||||
|
||||
/// A storage address. This allows access to a given storage entry, which can then
|
||||
/// be iterated over or fetched from by providing the relevant set of keys, or
|
||||
/// otherwise inspected.
|
||||
pub trait Address {
|
||||
/// All of the keys required to get to an individual value at this address.
|
||||
/// Keys must always impl [`IntoEncodableValues`], and for iteration must
|
||||
/// also impl [`frame_decode::storage::IntoDecodableValues`].
|
||||
type KeyParts: IntoEncodableValues + IntoDecodableValues;
|
||||
/// Type of the storage value at this location.
|
||||
type Value: DecodeAsType;
|
||||
/// Does the address point to a plain value (as opposed to a map)?
|
||||
/// Set to [`crate::utils::Yes`] to enable APIs which require a map,
|
||||
/// or [`crate::utils::Maybe`] to enable APIs which allow a map.
|
||||
type IsPlain: YesMaybe;
|
||||
|
||||
/// The pallet containing this storage entry.
|
||||
fn pallet_name(&self) -> &str;
|
||||
|
||||
/// The name of the storage entry.
|
||||
fn entry_name(&self) -> &str;
|
||||
|
||||
/// Return a unique hash for this address which can be used to validate it against metadata.
|
||||
fn validation_hash(&self) -> Option<[u8; 32]>;
|
||||
}
|
||||
|
||||
// Any reference to an address is a valid address.
|
||||
impl<A: Address + ?Sized> Address for &'_ A {
|
||||
type KeyParts = A::KeyParts;
|
||||
type Value = A::Value;
|
||||
type IsPlain = A::IsPlain;
|
||||
|
||||
fn pallet_name(&self) -> &str {
|
||||
A::pallet_name(*self)
|
||||
}
|
||||
|
||||
fn entry_name(&self) -> &str {
|
||||
A::entry_name(*self)
|
||||
}
|
||||
|
||||
fn validation_hash(&self) -> Option<[u8; 32]> {
|
||||
A::validation_hash(*self)
|
||||
}
|
||||
}
|
||||
|
||||
/// An address which is generated by the static APIs.
|
||||
pub struct StaticAddress<KeyParts, Value, IsPlain> {
|
||||
pallet_name: Cow<'static, str>,
|
||||
entry_name: Cow<'static, str>,
|
||||
validation_hash: Option<[u8; 32]>,
|
||||
marker: core::marker::PhantomData<(KeyParts, Value, IsPlain)>,
|
||||
}
|
||||
|
||||
impl<KeyParts, Value, IsPlain> Clone for StaticAddress<KeyParts, Value, IsPlain> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
pallet_name: self.pallet_name.clone(),
|
||||
entry_name: self.entry_name.clone(),
|
||||
validation_hash: self.validation_hash,
|
||||
marker: self.marker,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<KeyParts, Value, IsPlain> core::fmt::Debug for StaticAddress<KeyParts, Value, IsPlain> {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
f.debug_struct("StaticAddress")
|
||||
.field("pallet_name", &self.pallet_name)
|
||||
.field("entry_name", &self.entry_name)
|
||||
.field("validation_hash", &self.validation_hash)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<KeyParts, Value, IsPlain> StaticAddress<KeyParts, Value, IsPlain> {
|
||||
/// Create a new [`StaticAddress`] using static strings for the pallet and call name.
|
||||
/// This is only expected to be used from codegen.
|
||||
#[doc(hidden)]
|
||||
pub fn new_static(pallet_name: &'static str, entry_name: &'static str, hash: [u8; 32]) -> Self {
|
||||
Self {
|
||||
pallet_name: Cow::Borrowed(pallet_name),
|
||||
entry_name: Cow::Borrowed(entry_name),
|
||||
validation_hash: Some(hash),
|
||||
marker: core::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new address.
|
||||
pub fn new(pallet_name: impl Into<String>, entry_name: impl Into<String>) -> Self {
|
||||
Self {
|
||||
pallet_name: pallet_name.into().into(),
|
||||
entry_name: entry_name.into().into(),
|
||||
validation_hash: None,
|
||||
marker: core::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Do not validate this storage entry prior to accessing it.
|
||||
pub fn unvalidated(mut self) -> Self {
|
||||
self.validation_hash = None;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<KeyParts, Value, IsPlain> Address for StaticAddress<KeyParts, Value, IsPlain>
|
||||
where
|
||||
KeyParts: IntoEncodableValues + IntoDecodableValues,
|
||||
Value: DecodeAsType,
|
||||
IsPlain: YesMaybe,
|
||||
{
|
||||
type KeyParts = KeyParts;
|
||||
type Value = Value;
|
||||
type IsPlain = IsPlain;
|
||||
|
||||
fn pallet_name(&self) -> &str {
|
||||
&self.pallet_name
|
||||
}
|
||||
|
||||
fn entry_name(&self) -> &str {
|
||||
&self.entry_name
|
||||
}
|
||||
|
||||
fn validation_hash(&self) -> Option<[u8; 32]> {
|
||||
self.validation_hash
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: AsRef<str>, B: AsRef<str>> Address for (A, B) {
|
||||
type KeyParts = Vec<scale_value::Value>;
|
||||
type Value = scale_value::Value;
|
||||
type IsPlain = Maybe;
|
||||
|
||||
fn pallet_name(&self) -> &str {
|
||||
self.0.as_ref()
|
||||
}
|
||||
|
||||
fn entry_name(&self) -> &str {
|
||||
self.1.as_ref()
|
||||
}
|
||||
|
||||
fn validation_hash(&self) -> Option<[u8; 32]> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// A dynamic address is simply a [`StaticAddress`] which asserts that the
|
||||
/// entry *might* be a map and *might* have a default value.
|
||||
pub type DynamicAddress<KeyParts = Vec<scale_value::Value>, Value = scale_value::Value> =
|
||||
StaticAddress<KeyParts, Value, Maybe>;
|
||||
|
||||
/// Construct a new dynamic storage address. You can define the type of the
|
||||
/// storage keys and value yourself here, but have no guarantee that they will
|
||||
/// be correct.
|
||||
pub fn dynamic<KeyParts: IntoEncodableValues, Value: DecodeAsType>(
|
||||
pallet_name: impl Into<String>,
|
||||
entry_name: impl Into<String>,
|
||||
) -> DynamicAddress<KeyParts, Value> {
|
||||
DynamicAddress::<KeyParts, Value>::new(pallet_name.into(), entry_name.into())
|
||||
}
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! Encode storage keys, decode storage values, and validate static storage addresses.
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! ```rust
|
||||
//! use pezkuwi_subxt_signer::sr25519::dev;
|
||||
//! use pezkuwi_subxt_macro::subxt;
|
||||
//! use pezkuwi_subxt_core::storage;
|
||||
//! use pezkuwi_subxt_core::Metadata;
|
||||
//!
|
||||
//! // If we generate types without `subxt`, we need to point to `::pezkuwi_subxt_core`:
|
||||
//! #[subxt(
|
||||
//! crate = "::pezkuwi_subxt_core",
|
||||
//! runtime_metadata_path = "../artifacts/polkadot_metadata_small.scale",
|
||||
//! )]
|
||||
//! pub mod polkadot {}
|
||||
//!
|
||||
//! // Some metadata we'll use to work with storage entries:
|
||||
//! let metadata_bytes = include_bytes!("../../../artifacts/polkadot_metadata_small.scale");
|
||||
//! let metadata = Metadata::decode_from(&metadata_bytes[..]).unwrap();
|
||||
//!
|
||||
//! // Build a storage query to access account information.
|
||||
//! let address = polkadot::storage().system().account();
|
||||
//!
|
||||
//! // We can validate that the address is compatible with the given metadata.
|
||||
//! storage::validate(&address, &metadata).unwrap();
|
||||
//!
|
||||
//! // We can fetch details about the storage entry associated with this address:
|
||||
//! let entry = storage::entry(address, &metadata).unwrap();
|
||||
//!
|
||||
//! // .. including generating a key to fetch the entry with:
|
||||
//! let fetch_key = entry.fetch_key((dev::alice().public_key().into(),)).unwrap();
|
||||
//!
|
||||
//! // .. or generating a key to iterate over entries with at a given depth:
|
||||
//! let iter_key = entry.iter_key(()).unwrap();
|
||||
//!
|
||||
//! // Given a value, we can decode it:
|
||||
//! let value_bytes = hex::decode("00000000000000000100000000000000000064a7b3b6e00d0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080").unwrap();
|
||||
//! let value = entry.value(value_bytes).decode().unwrap();
|
||||
//!
|
||||
//! println!("Alice's account info: {value:?}");
|
||||
//! ```
|
||||
|
||||
mod prefix_of;
|
||||
mod storage_entry;
|
||||
mod storage_key;
|
||||
mod storage_key_value;
|
||||
mod storage_value;
|
||||
|
||||
pub mod address;
|
||||
|
||||
use crate::{Metadata, error::StorageError};
|
||||
use address::Address;
|
||||
use alloc::string::ToString;
|
||||
|
||||
pub use prefix_of::{EqualOrPrefixOf, PrefixOf};
|
||||
pub use storage_entry::{StorageEntry, entry};
|
||||
pub use storage_key::{StorageHasher, StorageKey, StorageKeyPart};
|
||||
pub use storage_key_value::StorageKeyValue;
|
||||
pub use storage_value::StorageValue;
|
||||
|
||||
/// When the provided `address` is statically generated via the `#[subxt]` macro, this validates
|
||||
/// that the shape of the storage value is the same as the shape expected by the static address.
|
||||
///
|
||||
/// When the provided `address` is dynamic (and thus does not come with any expectation of the
|
||||
/// shape of the constant value), this just returns `Ok(())`
|
||||
pub fn validate<Addr: Address>(address: Addr, metadata: &Metadata) -> Result<(), StorageError> {
|
||||
let Some(hash) = address.validation_hash() else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let pallet_name = address.pallet_name();
|
||||
let entry_name = address.entry_name();
|
||||
|
||||
let pallet_metadata = metadata
|
||||
.pallet_by_name(pallet_name)
|
||||
.ok_or_else(|| StorageError::PalletNameNotFound(pallet_name.to_string()))?;
|
||||
let storage_hash = pallet_metadata.storage_hash(entry_name).ok_or_else(|| {
|
||||
StorageError::StorageEntryNotFound {
|
||||
pallet_name: pallet_name.to_string(),
|
||||
entry_name: entry_name.to_string(),
|
||||
}
|
||||
})?;
|
||||
|
||||
if storage_hash != hash {
|
||||
Err(StorageError::IncompatibleCodegen)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
+195
@@ -0,0 +1,195 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
use alloc::vec::Vec;
|
||||
use frame_decode::helpers::IntoEncodableValues;
|
||||
use scale_encode::EncodeAsType;
|
||||
|
||||
/// For a given set of values that can be used as keys for a storage entry,
|
||||
/// this is implemented for any prefixes of that set. ie if the keys `(A,B,C)`
|
||||
/// would access a storage value, then `PrefixOf<(A,B,C)>` is implemented for
|
||||
/// `(A,B)`, `(A,)` and `()`.
|
||||
pub trait PrefixOf<Keys>: IntoEncodableValues {}
|
||||
|
||||
// If T impls PrefixOf<K>, &T impls PrefixOf<K>.
|
||||
impl<K, T: PrefixOf<K>> PrefixOf<K> for &T {}
|
||||
|
||||
// Impls for tuples up to length 6 (storage maps rarely require more than 2 entries
|
||||
// so it's very unlikely we'll ever need to go this deep).
|
||||
impl<A> PrefixOf<(A,)> for () {}
|
||||
|
||||
impl<A, B> PrefixOf<(A, B)> for () {}
|
||||
impl<A, B> PrefixOf<(A, B)> for (A,) where (A,): IntoEncodableValues {}
|
||||
|
||||
impl<A, B, C> PrefixOf<(A, B, C)> for () {}
|
||||
impl<A, B, C> PrefixOf<(A, B, C)> for (A,) where (A,): IntoEncodableValues {}
|
||||
impl<A, B, C> PrefixOf<(A, B, C)> for (A, B) where (A, B): IntoEncodableValues {}
|
||||
|
||||
impl<A, B, C, D> PrefixOf<(A, B, C, D)> for () {}
|
||||
impl<A, B, C, D> PrefixOf<(A, B, C, D)> for (A,) where (A,): IntoEncodableValues {}
|
||||
impl<A, B, C, D> PrefixOf<(A, B, C, D)> for (A, B) where (A, B): IntoEncodableValues {}
|
||||
impl<A, B, C, D> PrefixOf<(A, B, C, D)> for (A, B, C) where (A, B, C): IntoEncodableValues {}
|
||||
|
||||
impl<A, B, C, D, E> PrefixOf<(A, B, C, D, E)> for () {}
|
||||
impl<A, B, C, D, E> PrefixOf<(A, B, C, D, E)> for (A,) where (A,): IntoEncodableValues {}
|
||||
impl<A, B, C, D, E> PrefixOf<(A, B, C, D, E)> for (A, B) where (A, B): IntoEncodableValues {}
|
||||
impl<A, B, C, D, E> PrefixOf<(A, B, C, D, E)> for (A, B, C) where (A, B, C): IntoEncodableValues {}
|
||||
impl<A, B, C, D, E> PrefixOf<(A, B, C, D, E)> for (A, B, C, D) where
|
||||
(A, B, C, D): IntoEncodableValues
|
||||
{
|
||||
}
|
||||
|
||||
impl<A, B, C, D, E, F> PrefixOf<(A, B, C, D, E, F)> for () {}
|
||||
impl<A, B, C, D, E, F> PrefixOf<(A, B, C, D, E, F)> for (A,) where (A,): IntoEncodableValues {}
|
||||
impl<A, B, C, D, E, F> PrefixOf<(A, B, C, D, E, F)> for (A, B) where (A, B): IntoEncodableValues {}
|
||||
impl<A, B, C, D, E, F> PrefixOf<(A, B, C, D, E, F)> for (A, B, C) where
|
||||
(A, B, C): IntoEncodableValues
|
||||
{
|
||||
}
|
||||
impl<A, B, C, D, E, F> PrefixOf<(A, B, C, D, E, F)> for (A, B, C, D) where
|
||||
(A, B, C, D): IntoEncodableValues
|
||||
{
|
||||
}
|
||||
impl<A, B, C, D, E, F> PrefixOf<(A, B, C, D, E, F)> for (A, B, C, D, E) where
|
||||
(A, B, C, D, E): IntoEncodableValues
|
||||
{
|
||||
}
|
||||
|
||||
// Vecs are prefixes of vecs. The length is not statically known and so
|
||||
// these would be given dynamically only, leaving the correct length to the user.
|
||||
impl<T: EncodeAsType> PrefixOf<Vec<T>> for Vec<T> {}
|
||||
|
||||
// We don't use arrays in Subxt for storage entry access, but `IntoEncodableValues`
|
||||
// supports them so let's allow impls which do use them to benefit too.
|
||||
macro_rules! array_impl {
|
||||
($n:literal: $($p:literal)+) => {
|
||||
$(
|
||||
impl <T: EncodeAsType> PrefixOf<[T; $n]> for [T; $p] {}
|
||||
)+
|
||||
}
|
||||
}
|
||||
|
||||
array_impl!(1: 0);
|
||||
array_impl!(2: 1 0);
|
||||
array_impl!(3: 2 1 0);
|
||||
array_impl!(4: 3 2 1 0);
|
||||
array_impl!(5: 4 3 2 1 0);
|
||||
array_impl!(6: 5 4 3 2 1 0);
|
||||
|
||||
/// This is much like [`PrefixOf`] except that it also includes `Self` as an allowed type,
|
||||
/// where `Self` must impl [`IntoEncodableValues`] just as every [`PrefixOf<Self>`] does.
|
||||
pub trait EqualOrPrefixOf<K>: IntoEncodableValues {}
|
||||
|
||||
// Tuples
|
||||
macro_rules! tuple_impl_eq {
|
||||
($($t:ident)+) => {
|
||||
// Any T that is a PrefixOf<Keys> impls EqualOrPrefixOf<keys> too
|
||||
impl <$($t,)+ T: PrefixOf<($($t,)+)>> EqualOrPrefixOf<($($t,)+)> for T {}
|
||||
// Keys impls EqualOrPrefixOf<Keys>
|
||||
impl <$($t),+> EqualOrPrefixOf<($($t,)+)> for ($($t,)+) where ($($t,)+): IntoEncodableValues {}
|
||||
// &'a Keys impls EqualOrPrefixOf<Keys>
|
||||
impl <'a, $($t),+> EqualOrPrefixOf<($($t,)+)> for &'a ($($t,)+) where ($($t,)+): IntoEncodableValues {}
|
||||
}
|
||||
}
|
||||
|
||||
tuple_impl_eq!(A);
|
||||
tuple_impl_eq!(A B);
|
||||
tuple_impl_eq!(A B C);
|
||||
tuple_impl_eq!(A B C D);
|
||||
tuple_impl_eq!(A B C D E);
|
||||
tuple_impl_eq!(A B C D E F);
|
||||
|
||||
// Vec
|
||||
impl<T: EncodeAsType> EqualOrPrefixOf<Vec<T>> for Vec<T> {}
|
||||
impl<T: EncodeAsType> EqualOrPrefixOf<Vec<T>> for &Vec<T> {}
|
||||
|
||||
// Arrays
|
||||
macro_rules! array_impl_eq {
|
||||
($($n:literal)+) => {
|
||||
$(
|
||||
impl <A: EncodeAsType> EqualOrPrefixOf<[A; $n]> for [A; $n] {}
|
||||
impl <'a, A: EncodeAsType> EqualOrPrefixOf<[A; $n]> for &'a [A; $n] {}
|
||||
)+
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N: usize, A, T> EqualOrPrefixOf<[A; N]> for T where T: PrefixOf<[A; N]> {}
|
||||
array_impl_eq!(1 2 3 4 5 6);
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
struct Test<Keys: IntoEncodableValues>(core::marker::PhantomData<Keys>);
|
||||
|
||||
impl<Keys: IntoEncodableValues> Test<Keys> {
|
||||
fn new() -> Self {
|
||||
Test(core::marker::PhantomData)
|
||||
}
|
||||
fn accepts_prefix_of<P: PrefixOf<Keys>>(&self, keys: P) {
|
||||
let _encoder = keys.into_encodable_values();
|
||||
}
|
||||
fn accepts_eq_or_prefix_of<P: EqualOrPrefixOf<Keys>>(&self, keys: P) {
|
||||
let _encoder = keys.into_encodable_values();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_prefix_of() {
|
||||
// In real life we'd have a struct a bit like this:
|
||||
let t = Test::<(bool, String, u64)>::new();
|
||||
|
||||
// And we'd want to be able to call some method like this:
|
||||
//// This shouldn't work:
|
||||
// t.accepts_prefix_of((true, String::from("hi"), 0));
|
||||
t.accepts_prefix_of(&(true, String::from("hi")));
|
||||
t.accepts_prefix_of((true, String::from("hi")));
|
||||
t.accepts_prefix_of((true,));
|
||||
t.accepts_prefix_of(());
|
||||
|
||||
let t = Test::<[u64; 5]>::new();
|
||||
|
||||
//// This shouldn't work:
|
||||
// t.accepts_prefix_of([0,1,2,3,4]);
|
||||
t.accepts_prefix_of([0, 1, 2, 3]);
|
||||
t.accepts_prefix_of([0, 1, 2, 3]);
|
||||
t.accepts_prefix_of([0, 1, 2]);
|
||||
t.accepts_prefix_of([0, 1]);
|
||||
t.accepts_prefix_of([0]);
|
||||
t.accepts_prefix_of([]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_eq_or_prefix_of() {
|
||||
// In real life we'd have a struct a bit like this:
|
||||
let t = Test::<(bool, String, u64)>::new();
|
||||
|
||||
// And we'd want to be able to call some method like this:
|
||||
t.accepts_eq_or_prefix_of(&(true, String::from("hi"), 0));
|
||||
t.accepts_eq_or_prefix_of(&(true, String::from("hi")));
|
||||
t.accepts_eq_or_prefix_of((true,));
|
||||
t.accepts_eq_or_prefix_of(());
|
||||
|
||||
t.accepts_eq_or_prefix_of((true, String::from("hi"), 0));
|
||||
t.accepts_eq_or_prefix_of((true, String::from("hi")));
|
||||
t.accepts_eq_or_prefix_of((true,));
|
||||
t.accepts_eq_or_prefix_of(());
|
||||
|
||||
let t = Test::<[u64; 5]>::new();
|
||||
|
||||
t.accepts_eq_or_prefix_of([0, 1, 2, 3, 4]);
|
||||
t.accepts_eq_or_prefix_of([0, 1, 2, 3]);
|
||||
t.accepts_eq_or_prefix_of([0, 1, 2]);
|
||||
t.accepts_eq_or_prefix_of([0, 1]);
|
||||
t.accepts_eq_or_prefix_of([0]);
|
||||
t.accepts_eq_or_prefix_of([]);
|
||||
|
||||
t.accepts_eq_or_prefix_of([0, 1, 2, 3, 4]);
|
||||
t.accepts_eq_or_prefix_of([0, 1, 2, 3]);
|
||||
t.accepts_eq_or_prefix_of([0, 1, 2]);
|
||||
t.accepts_eq_or_prefix_of([0, 1]);
|
||||
t.accepts_eq_or_prefix_of([0]);
|
||||
t.accepts_eq_or_prefix_of([]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
use super::{PrefixOf, StorageKeyValue, StorageValue, address::Address};
|
||||
use crate::error::StorageError;
|
||||
use crate::utils::YesMaybe;
|
||||
use alloc::sync::Arc;
|
||||
use alloc::vec::Vec;
|
||||
use frame_decode::storage::{IntoEncodableValues, StorageInfo};
|
||||
use scale_info::PortableRegistry;
|
||||
use pezkuwi_subxt_metadata::Metadata;
|
||||
|
||||
/// Create a [`StorageEntry`] to work with a given storage entry.
|
||||
pub fn entry<'info, Addr: Address>(
|
||||
address: Addr,
|
||||
metadata: &'info Metadata,
|
||||
) -> Result<StorageEntry<'info, Addr>, StorageError> {
|
||||
super::validate(&address, metadata)?;
|
||||
|
||||
use frame_decode::storage::StorageTypeInfo;
|
||||
let types = metadata.types();
|
||||
let info = metadata
|
||||
.storage_info(address.pallet_name(), address.entry_name())
|
||||
.map_err(|e| StorageError::StorageInfoError(e.into_owned()))?;
|
||||
|
||||
Ok(StorageEntry(Arc::new(StorageEntryInner {
|
||||
address,
|
||||
info: Arc::new(info),
|
||||
types,
|
||||
})))
|
||||
}
|
||||
|
||||
/// This represents a single storage entry (be it a plain value or map).
|
||||
pub struct StorageEntry<'info, Addr>(Arc<StorageEntryInner<'info, Addr>>);
|
||||
|
||||
impl<'info, Addr> Clone for StorageEntry<'info, Addr> {
|
||||
fn clone(&self) -> Self {
|
||||
Self(self.0.clone())
|
||||
}
|
||||
}
|
||||
|
||||
struct StorageEntryInner<'info, Addr> {
|
||||
address: Addr,
|
||||
info: Arc<StorageInfo<'info, u32>>,
|
||||
types: &'info PortableRegistry,
|
||||
}
|
||||
|
||||
impl<'info, Addr: Address> StorageEntry<'info, Addr> {
|
||||
/// Name of the pallet containing this storage entry.
|
||||
pub fn pallet_name(&self) -> &str {
|
||||
self.0.address.pallet_name()
|
||||
}
|
||||
|
||||
/// Name of the storage entry.
|
||||
pub fn entry_name(&self) -> &str {
|
||||
self.0.address.entry_name()
|
||||
}
|
||||
|
||||
/// Is the storage entry a plain value?
|
||||
pub fn is_plain(&self) -> bool {
|
||||
self.0.info.keys.is_empty()
|
||||
}
|
||||
|
||||
/// Is the storage entry a map?
|
||||
pub fn is_map(&self) -> bool {
|
||||
!self.is_plain()
|
||||
}
|
||||
|
||||
/// Instantiate a [`StorageKeyValue`] for this entry.
|
||||
///
|
||||
/// It is expected that the bytes are obtained by iterating key/value pairs at this address.
|
||||
pub fn key_value(
|
||||
&self,
|
||||
key_bytes: impl Into<Arc<[u8]>>,
|
||||
value_bytes: Vec<u8>,
|
||||
) -> StorageKeyValue<'info, Addr> {
|
||||
StorageKeyValue::new(
|
||||
self.0.info.clone(),
|
||||
self.0.types,
|
||||
key_bytes.into(),
|
||||
value_bytes,
|
||||
)
|
||||
}
|
||||
|
||||
/// Instantiate a [`StorageValue`] for this entry.
|
||||
///
|
||||
/// It is expected that the bytes are obtained by fetching a value at this address.
|
||||
pub fn value(&self, bytes: Vec<u8>) -> StorageValue<'info, Addr::Value> {
|
||||
StorageValue::new(self.0.info.clone(), self.0.types, bytes)
|
||||
}
|
||||
|
||||
/// Return the default [`StorageValue`] for this storage entry, if there is one.
|
||||
pub fn default_value(&self) -> Option<StorageValue<'info, Addr::Value>> {
|
||||
self.0.info.default_value.as_deref().map(|default_bytes| {
|
||||
StorageValue::new(self.0.info.clone(), self.0.types, default_bytes.to_vec())
|
||||
})
|
||||
}
|
||||
|
||||
/// The keys for plain storage values are always 32 byte hashes.
|
||||
pub fn key_prefix(&self) -> [u8; 32] {
|
||||
frame_decode::storage::encode_storage_key_prefix(
|
||||
self.0.address.pallet_name(),
|
||||
self.0.address.entry_name(),
|
||||
)
|
||||
}
|
||||
|
||||
// This has a less "strict" type signature and so is just used under the hood.
|
||||
fn key<Keys: IntoEncodableValues>(&self, key_parts: Keys) -> Result<Vec<u8>, StorageError> {
|
||||
let key = frame_decode::storage::encode_storage_key_with_info(
|
||||
self.0.address.pallet_name(),
|
||||
self.0.address.entry_name(),
|
||||
key_parts,
|
||||
&self.0.info,
|
||||
self.0.types,
|
||||
)
|
||||
.map_err(StorageError::StorageKeyEncodeError)?;
|
||||
|
||||
Ok(key)
|
||||
}
|
||||
|
||||
/// This constructs a key suitable for fetching a value at the given map storage address. This will error
|
||||
/// if we can see that the wrong number of key parts are provided.
|
||||
pub fn fetch_key(&self, key_parts: Addr::KeyParts) -> Result<Vec<u8>, StorageError> {
|
||||
if key_parts.num_encodable_values() != self.0.info.keys.len() {
|
||||
Err(StorageError::WrongNumberOfKeyPartsProvidedForFetching {
|
||||
expected: self.0.info.keys.len(),
|
||||
got: key_parts.num_encodable_values(),
|
||||
})
|
||||
} else {
|
||||
self.key(key_parts)
|
||||
}
|
||||
}
|
||||
|
||||
/// This constructs a key suitable for iterating at the given storage address. This will error
|
||||
/// if we can see that too many key parts are provided.
|
||||
pub fn iter_key<Keys: PrefixOf<Addr::KeyParts>>(
|
||||
&self,
|
||||
key_parts: Keys,
|
||||
) -> Result<Vec<u8>, StorageError> {
|
||||
if Addr::IsPlain::is_yes() {
|
||||
Err(StorageError::CannotIterPlainEntry {
|
||||
pallet_name: self.0.address.pallet_name().into(),
|
||||
entry_name: self.0.address.entry_name().into(),
|
||||
})
|
||||
} else if key_parts.num_encodable_values() >= self.0.info.keys.len() {
|
||||
Err(StorageError::WrongNumberOfKeyPartsProvidedForIterating {
|
||||
max_expected: self.0.info.keys.len() - 1,
|
||||
got: key_parts.num_encodable_values(),
|
||||
})
|
||||
} else {
|
||||
self.key(key_parts)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
use crate::error::StorageKeyError;
|
||||
use alloc::sync::Arc;
|
||||
use core::marker::PhantomData;
|
||||
use frame_decode::storage::{IntoDecodableValues, StorageInfo, StorageKey as StorageKeyPartInfo};
|
||||
use scale_info::PortableRegistry;
|
||||
|
||||
pub use frame_decode::storage::StorageHasher;
|
||||
|
||||
/// This represents the different parts of a storage key.
|
||||
pub struct StorageKey<'info, KeyParts> {
|
||||
info: Arc<StorageKeyPartInfo<u32>>,
|
||||
types: &'info PortableRegistry,
|
||||
bytes: Arc<[u8]>,
|
||||
marker: PhantomData<KeyParts>,
|
||||
}
|
||||
|
||||
impl<'info, KeyParts: IntoDecodableValues> StorageKey<'info, KeyParts> {
|
||||
pub(crate) fn new(
|
||||
info: &StorageInfo<'info, u32>,
|
||||
types: &'info PortableRegistry,
|
||||
bytes: Arc<[u8]>,
|
||||
) -> Result<Self, StorageKeyError> {
|
||||
let cursor = &mut &*bytes;
|
||||
let storage_key_info = frame_decode::storage::decode_storage_key_with_info(
|
||||
cursor, info, types,
|
||||
)
|
||||
.map_err(|e| StorageKeyError::StorageKeyDecodeError {
|
||||
bytes: bytes.to_vec(),
|
||||
error: e,
|
||||
})?;
|
||||
|
||||
if !cursor.is_empty() {
|
||||
return Err(StorageKeyError::LeftoverBytes {
|
||||
bytes: cursor.to_vec(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(StorageKey {
|
||||
info: Arc::new(storage_key_info),
|
||||
types,
|
||||
bytes,
|
||||
marker: PhantomData,
|
||||
})
|
||||
}
|
||||
|
||||
/// Attempt to decode the values contained within this storage key. The target type is
|
||||
/// given by the storage address used to access this entry. To decode into a custom type,
|
||||
/// use [`Self::parts()`] or [`Self::part()`] and decode each part.
|
||||
pub fn decode(&self) -> Result<KeyParts, StorageKeyError> {
|
||||
let values =
|
||||
frame_decode::storage::decode_storage_key_values(&self.bytes, &self.info, self.types)
|
||||
.map_err(StorageKeyError::CannotDecodeValuesInKey)?;
|
||||
|
||||
Ok(values)
|
||||
}
|
||||
|
||||
/// Iterate over the parts of this storage key. Each part of a storage key corresponds to a
|
||||
/// single value that has been hashed.
|
||||
pub fn parts(&self) -> impl ExactSizeIterator<Item = StorageKeyPart<'info>> {
|
||||
let parts_len = self.info.parts().len();
|
||||
(0..parts_len).map(move |index| StorageKeyPart {
|
||||
index,
|
||||
info: self.info.clone(),
|
||||
types: self.types,
|
||||
bytes: self.bytes.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Return the part of the storage key at the provided index, or `None` if the index is out of bounds.
|
||||
pub fn part(&self, index: usize) -> Option<StorageKeyPart<'info>> {
|
||||
if index < self.parts().len() {
|
||||
Some(StorageKeyPart {
|
||||
index,
|
||||
info: self.info.clone(),
|
||||
types: self.types,
|
||||
bytes: self.bytes.clone(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This represents a part of a storage key.
|
||||
pub struct StorageKeyPart<'info> {
|
||||
index: usize,
|
||||
info: Arc<StorageKeyPartInfo<u32>>,
|
||||
types: &'info PortableRegistry,
|
||||
bytes: Arc<[u8]>,
|
||||
}
|
||||
|
||||
impl<'info> StorageKeyPart<'info> {
|
||||
/// Get the raw bytes for this part of the storage key.
|
||||
pub fn bytes(&self) -> &[u8] {
|
||||
let part = &self.info[self.index];
|
||||
let hash_range = part.hash_range();
|
||||
let value_range = part.value().map(|v| v.range()).unwrap_or(core::ops::Range {
|
||||
start: hash_range.end,
|
||||
end: hash_range.end,
|
||||
});
|
||||
let combined_range = core::ops::Range {
|
||||
start: hash_range.start,
|
||||
end: value_range.end,
|
||||
};
|
||||
&self.bytes[combined_range]
|
||||
}
|
||||
|
||||
/// Get the hasher that was used to construct this part of the storage key.
|
||||
pub fn hasher(&self) -> StorageHasher {
|
||||
self.info[self.index].hasher()
|
||||
}
|
||||
|
||||
/// For keys that were produced using "concat" or "identity" hashers, the value
|
||||
/// is available as a part of the key hash, allowing us to decode it into anything
|
||||
/// implementing [`scale_decode::DecodeAsType`]. If the key was produced using a
|
||||
/// different hasher, this will return `None`.
|
||||
pub fn decode_as<T: scale_decode::DecodeAsType>(&self) -> Result<Option<T>, StorageKeyError> {
|
||||
let part_info = &self.info[self.index];
|
||||
let Some(value_info) = part_info.value() else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let value_bytes = &self.bytes[value_info.range()];
|
||||
let value_ty = *value_info.ty();
|
||||
|
||||
let decoded_key_part = T::decode_as_type(&mut &*value_bytes, value_ty, self.types)
|
||||
.map_err(|e| StorageKeyError::CannotDecodeValueInKey {
|
||||
index: self.index,
|
||||
error: e,
|
||||
})?;
|
||||
|
||||
Ok(Some(decoded_key_part))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
use super::{Address, StorageKey, StorageValue};
|
||||
use crate::error::StorageKeyError;
|
||||
use alloc::sync::Arc;
|
||||
use alloc::vec::Vec;
|
||||
use frame_decode::storage::StorageInfo;
|
||||
use scale_info::PortableRegistry;
|
||||
|
||||
/// This represents a storage key/value pair, which is typically returned from
|
||||
/// iterating over values in some storage map.
|
||||
#[derive(Debug)]
|
||||
pub struct StorageKeyValue<'info, Addr: Address> {
|
||||
key: Arc<[u8]>,
|
||||
// This contains the storage information already:
|
||||
value: StorageValue<'info, Addr::Value>,
|
||||
}
|
||||
|
||||
impl<'info, Addr: Address> StorageKeyValue<'info, Addr> {
|
||||
pub(crate) fn new(
|
||||
info: Arc<StorageInfo<'info, u32>>,
|
||||
types: &'info PortableRegistry,
|
||||
key_bytes: Arc<[u8]>,
|
||||
value_bytes: Vec<u8>,
|
||||
) -> Self {
|
||||
StorageKeyValue {
|
||||
key: key_bytes,
|
||||
value: StorageValue::new(info, types, value_bytes),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the raw bytes for this storage entry's key.
|
||||
pub fn key_bytes(&self) -> &[u8] {
|
||||
&self.key
|
||||
}
|
||||
|
||||
/// Decode the key for this storage entry. This gives back a type from which we can
|
||||
/// decode specific parts of the key hash (where applicable).
|
||||
pub fn key(&'_ self) -> Result<StorageKey<'info, Addr::KeyParts>, StorageKeyError> {
|
||||
StorageKey::new(&self.value.info, self.value.types, self.key.clone())
|
||||
}
|
||||
|
||||
/// Return the storage value.
|
||||
pub fn value(&self) -> &StorageValue<'info, Addr::Value> {
|
||||
&self.value
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
use crate::error::StorageValueError;
|
||||
use alloc::sync::Arc;
|
||||
use alloc::vec::Vec;
|
||||
use core::marker::PhantomData;
|
||||
use frame_decode::storage::StorageInfo;
|
||||
use scale_decode::DecodeAsType;
|
||||
use scale_info::PortableRegistry;
|
||||
|
||||
/// This represents a storage value.
|
||||
#[derive(Debug)]
|
||||
pub struct StorageValue<'info, Value> {
|
||||
pub(crate) info: Arc<StorageInfo<'info, u32>>,
|
||||
pub(crate) types: &'info PortableRegistry,
|
||||
bytes: Vec<u8>,
|
||||
marker: PhantomData<Value>,
|
||||
}
|
||||
|
||||
impl<'info, Value: DecodeAsType> StorageValue<'info, Value> {
|
||||
pub(crate) fn new(
|
||||
info: Arc<StorageInfo<'info, u32>>,
|
||||
types: &'info PortableRegistry,
|
||||
bytes: Vec<u8>,
|
||||
) -> StorageValue<'info, Value> {
|
||||
StorageValue {
|
||||
info,
|
||||
types,
|
||||
bytes,
|
||||
marker: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the raw bytes for this storage value.
|
||||
pub fn bytes(&self) -> &[u8] {
|
||||
&self.bytes
|
||||
}
|
||||
|
||||
/// Consume this storage value and return the raw bytes.
|
||||
pub fn into_bytes(self) -> Vec<u8> {
|
||||
self.bytes.to_vec()
|
||||
}
|
||||
|
||||
/// Decode this storage value into the provided response type.
|
||||
pub fn decode(&self) -> Result<Value, StorageValueError> {
|
||||
self.decode_as::<Value>()
|
||||
}
|
||||
|
||||
/// Decode this storage value into an arbitrary type.
|
||||
pub fn decode_as<T: DecodeAsType>(&self) -> Result<T, StorageValueError> {
|
||||
let cursor = &mut &*self.bytes;
|
||||
|
||||
let value = frame_decode::storage::decode_storage_value_with_info(
|
||||
cursor,
|
||||
&self.info,
|
||||
self.types,
|
||||
T::into_visitor(),
|
||||
)
|
||||
.map_err(StorageValueError::CannotDecode)?;
|
||||
|
||||
if !cursor.is_empty() {
|
||||
return Err(StorageValueError::LeftoverBytes {
|
||||
bytes: cursor.to_vec(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(value)
|
||||
}
|
||||
}
|
||||
+458
@@ -0,0 +1,458 @@
|
||||
// Copyright 2019-2024 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! Construct and sign transactions.
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! ```rust
|
||||
//! use pezkuwi_subxt_signer::sr25519::dev;
|
||||
//! use pezkuwi_subxt_macro::subxt;
|
||||
//! use pezkuwi_subxt_core::config::{PolkadotConfig, HashFor};
|
||||
//! use pezkuwi_subxt_core::config::DefaultExtrinsicParamsBuilder as Params;
|
||||
//! use pezkuwi_subxt_core::tx;
|
||||
//! use pezkuwi_subxt_core::utils::H256;
|
||||
//! use pezkuwi_subxt_core::Metadata;
|
||||
//!
|
||||
//! // If we generate types without `subxt`, we need to point to `::pezkuwi_subxt_core`:
|
||||
//! #[subxt(
|
||||
//! crate = "::pezkuwi_subxt_core",
|
||||
//! runtime_metadata_path = "../artifacts/polkadot_metadata_small.scale",
|
||||
//! )]
|
||||
//! pub mod polkadot {}
|
||||
//!
|
||||
//! // Gather some other information about the chain that we'll need to construct valid extrinsics:
|
||||
//! let state = tx::ClientState::<PolkadotConfig> {
|
||||
//! metadata: {
|
||||
//! let metadata_bytes = include_bytes!("../../../artifacts/polkadot_metadata_small.scale");
|
||||
//! Metadata::decode_from(&metadata_bytes[..]).unwrap()
|
||||
//! },
|
||||
//! genesis_hash: {
|
||||
//! let h = "91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3";
|
||||
//! let bytes = hex::decode(h).unwrap();
|
||||
//! H256::from_slice(&bytes)
|
||||
//! },
|
||||
//! runtime_version: tx::RuntimeVersion {
|
||||
//! spec_version: 9370,
|
||||
//! transaction_version: 20,
|
||||
//! }
|
||||
//! };
|
||||
//!
|
||||
//! // Now we can build a balance transfer extrinsic.
|
||||
//! let dest = dev::bob().public_key().into();
|
||||
//! let call = polkadot::tx().balances().transfer_allow_death(dest, 10_000);
|
||||
//! let params = Params::new().tip(1_000).nonce(0).build();
|
||||
//!
|
||||
//! // We can validate that this lines up with the given metadata:
|
||||
//! tx::validate(&call, &state.metadata).unwrap();
|
||||
//!
|
||||
//! // We can build a signed transaction:
|
||||
//! let signed_call = tx::create_v4_signed(&call, &state, params)
|
||||
//! .unwrap()
|
||||
//! .sign(&dev::alice());
|
||||
//!
|
||||
//! // And log it:
|
||||
//! println!("Tx: 0x{}", hex::encode(signed_call.encoded()));
|
||||
//! ```
|
||||
|
||||
pub mod payload;
|
||||
pub mod signer;
|
||||
|
||||
use crate::Metadata;
|
||||
use crate::config::{Config, ExtrinsicParams, ExtrinsicParamsEncoder, HashFor, Hasher};
|
||||
use crate::error::ExtrinsicError;
|
||||
use crate::utils::Encoded;
|
||||
use alloc::borrow::Cow;
|
||||
use alloc::string::ToString;
|
||||
use alloc::vec::Vec;
|
||||
use codec::{Compact, Encode};
|
||||
use payload::Payload;
|
||||
use signer::Signer as SignerT;
|
||||
use pezsp_crypto_hashing::blake2_256;
|
||||
|
||||
// Expose these here since we expect them in some calls below.
|
||||
pub use crate::client::{ClientState, RuntimeVersion};
|
||||
|
||||
/// Run the validation logic against some extrinsic you'd like to submit. Returns `Ok(())`
|
||||
/// if the call is valid (or if it's not possible to check since the call has no validation hash).
|
||||
/// Return an error if the call was not valid or something went wrong trying to validate it (ie
|
||||
/// the pallet or call in question do not exist at all).
|
||||
pub fn validate<Call: Payload>(call: &Call, metadata: &Metadata) -> Result<(), ExtrinsicError> {
|
||||
let Some(details) = call.validation_details() else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let pallet_name = details.pallet_name;
|
||||
let call_name = details.call_name;
|
||||
|
||||
let expected_hash = metadata
|
||||
.pallet_by_name(pallet_name)
|
||||
.ok_or_else(|| ExtrinsicError::PalletNameNotFound(pallet_name.to_string()))?
|
||||
.call_hash(call_name)
|
||||
.ok_or_else(|| ExtrinsicError::CallNameNotFound {
|
||||
pallet_name: pallet_name.to_string(),
|
||||
call_name: call_name.to_string(),
|
||||
})?;
|
||||
|
||||
if details.hash != expected_hash {
|
||||
Err(ExtrinsicError::IncompatibleCodegen)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the suggested transaction versions to build for a given chain, or an error
|
||||
/// if Subxt doesn't support any version expected by the chain.
|
||||
///
|
||||
/// If the result is [`TransactionVersion::V4`], use the `v4` methods in this module. If it's
|
||||
/// [`TransactionVersion::V5`], use the `v5` ones.
|
||||
pub fn suggested_version(metadata: &Metadata) -> Result<TransactionVersion, ExtrinsicError> {
|
||||
let versions = metadata.extrinsic().supported_versions();
|
||||
|
||||
if versions.contains(&4) {
|
||||
Ok(TransactionVersion::V4)
|
||||
} else if versions.contains(&5) {
|
||||
Ok(TransactionVersion::V5)
|
||||
} else {
|
||||
Err(ExtrinsicError::UnsupportedVersion)
|
||||
}
|
||||
}
|
||||
|
||||
/// The transaction versions supported by Subxt.
|
||||
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
|
||||
pub enum TransactionVersion {
|
||||
/// v4 transactions (signed and unsigned transactions)
|
||||
V4,
|
||||
/// v5 transactions (bare and general transactions)
|
||||
V5,
|
||||
}
|
||||
|
||||
/// Return the SCALE encoded bytes representing the call data of the transaction.
|
||||
pub fn call_data<Call: Payload>(
|
||||
call: &Call,
|
||||
metadata: &Metadata,
|
||||
) -> Result<Vec<u8>, ExtrinsicError> {
|
||||
let mut bytes = Vec::new();
|
||||
call.encode_call_data_to(metadata, &mut bytes)?;
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
/// Creates a V4 "unsigned" transaction without submitting it.
|
||||
pub fn create_v4_unsigned<T: Config, Call: Payload>(
|
||||
call: &Call,
|
||||
metadata: &Metadata,
|
||||
) -> Result<Transaction<T>, ExtrinsicError> {
|
||||
create_unsigned_at_version(call, 4, metadata)
|
||||
}
|
||||
|
||||
/// Creates a V5 "bare" transaction without submitting it.
|
||||
pub fn create_v5_bare<T: Config, Call: Payload>(
|
||||
call: &Call,
|
||||
metadata: &Metadata,
|
||||
) -> Result<Transaction<T>, ExtrinsicError> {
|
||||
create_unsigned_at_version(call, 5, metadata)
|
||||
}
|
||||
|
||||
// Create a V4 "unsigned" transaction or V5 "bare" transaction.
|
||||
fn create_unsigned_at_version<T: Config, Call: Payload>(
|
||||
call: &Call,
|
||||
tx_version: u8,
|
||||
metadata: &Metadata,
|
||||
) -> Result<Transaction<T>, ExtrinsicError> {
|
||||
// 1. Validate this call against the current node metadata if the call comes
|
||||
// with a hash allowing us to do so.
|
||||
validate(call, metadata)?;
|
||||
|
||||
// 2. Encode extrinsic
|
||||
let extrinsic = {
|
||||
let mut encoded_inner = Vec::new();
|
||||
// encode the transaction version first.
|
||||
tx_version.encode_to(&mut encoded_inner);
|
||||
// encode call data after this byte.
|
||||
call.encode_call_data_to(metadata, &mut encoded_inner)?;
|
||||
// now, prefix byte length:
|
||||
let len = Compact(
|
||||
u32::try_from(encoded_inner.len()).expect("extrinsic size expected to be <4GB"),
|
||||
);
|
||||
let mut encoded = Vec::new();
|
||||
len.encode_to(&mut encoded);
|
||||
encoded.extend(encoded_inner);
|
||||
encoded
|
||||
};
|
||||
|
||||
// Wrap in Encoded to ensure that any more "encode" calls leave it in the right state.
|
||||
Ok(Transaction::from_bytes(extrinsic))
|
||||
}
|
||||
|
||||
/// Construct a v4 extrinsic, ready to be signed.
|
||||
pub fn create_v4_signed<T: Config, Call: Payload>(
|
||||
call: &Call,
|
||||
client_state: &ClientState<T>,
|
||||
params: <T::ExtrinsicParams as ExtrinsicParams<T>>::Params,
|
||||
) -> Result<PartialTransactionV4<T>, ExtrinsicError> {
|
||||
// 1. Validate this call against the current node metadata if the call comes
|
||||
// with a hash allowing us to do so.
|
||||
validate(call, &client_state.metadata)?;
|
||||
|
||||
// 2. SCALE encode call data to bytes (pallet u8, call u8, call params).
|
||||
let call_data = call_data(call, &client_state.metadata)?;
|
||||
|
||||
// 3. Construct our custom additional/extra params.
|
||||
let additional_and_extra_params =
|
||||
<T::ExtrinsicParams as ExtrinsicParams<T>>::new(client_state, params)?;
|
||||
|
||||
// Return these details, ready to construct a signed extrinsic from.
|
||||
Ok(PartialTransactionV4 {
|
||||
call_data,
|
||||
additional_and_extra_params,
|
||||
})
|
||||
}
|
||||
|
||||
/// Construct a v5 "general" extrinsic, ready to be signed or emitted as is.
|
||||
pub fn create_v5_general<T: Config, Call: Payload>(
|
||||
call: &Call,
|
||||
client_state: &ClientState<T>,
|
||||
params: <T::ExtrinsicParams as ExtrinsicParams<T>>::Params,
|
||||
) -> Result<PartialTransactionV5<T>, ExtrinsicError> {
|
||||
// 1. Validate this call against the current node metadata if the call comes
|
||||
// with a hash allowing us to do so.
|
||||
validate(call, &client_state.metadata)?;
|
||||
|
||||
// 2. Work out which TX extension version to target based on metadata.
|
||||
let tx_extensions_version = client_state
|
||||
.metadata
|
||||
.extrinsic()
|
||||
.transaction_extension_version_to_use_for_encoding();
|
||||
|
||||
// 3. SCALE encode call data to bytes (pallet u8, call u8, call params).
|
||||
let call_data = call_data(call, &client_state.metadata)?;
|
||||
|
||||
// 4. Construct our custom additional/extra params.
|
||||
let additional_and_extra_params =
|
||||
<T::ExtrinsicParams as ExtrinsicParams<T>>::new(client_state, params)?;
|
||||
|
||||
// Return these details, ready to construct a signed extrinsic from.
|
||||
Ok(PartialTransactionV5 {
|
||||
call_data,
|
||||
additional_and_extra_params,
|
||||
tx_extensions_version,
|
||||
})
|
||||
}
|
||||
|
||||
/// A partially constructed V4 extrinsic, ready to be signed.
|
||||
pub struct PartialTransactionV4<T: Config> {
|
||||
call_data: Vec<u8>,
|
||||
additional_and_extra_params: T::ExtrinsicParams,
|
||||
}
|
||||
|
||||
impl<T: Config> PartialTransactionV4<T> {
|
||||
/// Return the bytes representing the call data for this partially constructed
|
||||
/// extrinsic.
|
||||
pub fn call_data(&self) -> &[u8] {
|
||||
&self.call_data
|
||||
}
|
||||
|
||||
// Obtain bytes representing the signer payload and run call some function
|
||||
// with them. This can avoid an allocation in some cases.
|
||||
fn with_signer_payload<F, R>(&self, f: F) -> R
|
||||
where
|
||||
F: for<'a> FnOnce(Cow<'a, [u8]>) -> R,
|
||||
{
|
||||
let mut bytes = self.call_data.clone();
|
||||
self.additional_and_extra_params
|
||||
.encode_signer_payload_value_to(&mut bytes);
|
||||
self.additional_and_extra_params
|
||||
.encode_implicit_to(&mut bytes);
|
||||
|
||||
if bytes.len() > 256 {
|
||||
f(Cow::Borrowed(&blake2_256(&bytes)))
|
||||
} else {
|
||||
f(Cow::Owned(bytes))
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the V4 signer payload for this extrinsic. These are the bytes that must
|
||||
/// be signed in order to produce a valid signature for the extrinsic.
|
||||
pub fn signer_payload(&self) -> Vec<u8> {
|
||||
self.with_signer_payload(|bytes| bytes.to_vec())
|
||||
}
|
||||
|
||||
/// Convert this [`PartialTransactionV4`] into a V4 signed [`Transaction`], ready to submit.
|
||||
/// The provided `signer` is responsible for providing the "from" address for the transaction,
|
||||
/// as well as providing a signature to attach to it.
|
||||
pub fn sign<Signer>(&self, signer: &Signer) -> Transaction<T>
|
||||
where
|
||||
Signer: SignerT<T>,
|
||||
{
|
||||
// Given our signer, we can sign the payload representing this extrinsic.
|
||||
let signature = self.with_signer_payload(|bytes| signer.sign(&bytes));
|
||||
// Now, use the signature and "from" address to build the extrinsic.
|
||||
self.sign_with_account_and_signature(signer.account_id(), &signature)
|
||||
}
|
||||
|
||||
/// Convert this [`PartialTransactionV4`] into a V4 signed [`Transaction`], ready to submit.
|
||||
/// The provided `address` and `signature` will be used.
|
||||
pub fn sign_with_account_and_signature(
|
||||
&self,
|
||||
account_id: T::AccountId,
|
||||
signature: &T::Signature,
|
||||
) -> Transaction<T> {
|
||||
let extrinsic = {
|
||||
let mut encoded_inner = Vec::new();
|
||||
// "is signed" + transaction protocol version (4)
|
||||
(0b10000000 + 4u8).encode_to(&mut encoded_inner);
|
||||
// from address for signature
|
||||
let address: T::Address = account_id.into();
|
||||
address.encode_to(&mut encoded_inner);
|
||||
// the signature
|
||||
signature.encode_to(&mut encoded_inner);
|
||||
// attach custom extra params
|
||||
self.additional_and_extra_params
|
||||
.encode_value_to(&mut encoded_inner);
|
||||
// and now, call data (remembering that it's been encoded already and just needs appending)
|
||||
encoded_inner.extend(&self.call_data);
|
||||
// now, prefix byte length:
|
||||
let len = Compact(
|
||||
u32::try_from(encoded_inner.len()).expect("extrinsic size expected to be <4GB"),
|
||||
);
|
||||
let mut encoded = Vec::new();
|
||||
len.encode_to(&mut encoded);
|
||||
encoded.extend(encoded_inner);
|
||||
encoded
|
||||
};
|
||||
|
||||
// Return an extrinsic ready to be submitted.
|
||||
Transaction::from_bytes(extrinsic)
|
||||
}
|
||||
}
|
||||
|
||||
/// A partially constructed V5 general extrinsic, ready to be signed or emitted as-is.
|
||||
pub struct PartialTransactionV5<T: Config> {
|
||||
call_data: Vec<u8>,
|
||||
additional_and_extra_params: T::ExtrinsicParams,
|
||||
tx_extensions_version: u8,
|
||||
}
|
||||
|
||||
impl<T: Config> PartialTransactionV5<T> {
|
||||
/// Return the bytes representing the call data for this partially constructed
|
||||
/// extrinsic.
|
||||
pub fn call_data(&self) -> &[u8] {
|
||||
&self.call_data
|
||||
}
|
||||
|
||||
/// Return the V5 signer payload for this extrinsic. These are the bytes that must
|
||||
/// be signed in order to produce a valid signature for the extrinsic.
|
||||
pub fn signer_payload(&self) -> [u8; 32] {
|
||||
let mut bytes = self.call_data.clone();
|
||||
|
||||
self.additional_and_extra_params
|
||||
.encode_signer_payload_value_to(&mut bytes);
|
||||
self.additional_and_extra_params
|
||||
.encode_implicit_to(&mut bytes);
|
||||
|
||||
blake2_256(&bytes)
|
||||
}
|
||||
|
||||
/// Convert this [`PartialTransactionV5`] into a V5 "general" [`Transaction`].
|
||||
///
|
||||
/// This transaction has not been explicitly signed. Use [`Self::sign`]
|
||||
/// or [`Self::sign_with_account_and_signature`] if you wish to provide a
|
||||
/// signature (this is usually a necessary step).
|
||||
pub fn to_transaction(&self) -> Transaction<T> {
|
||||
let extrinsic = {
|
||||
let mut encoded_inner = Vec::new();
|
||||
// "is general" + transaction protocol version (5)
|
||||
(0b01000000 + 5u8).encode_to(&mut encoded_inner);
|
||||
// Encode versions for the transaction extensions
|
||||
self.tx_extensions_version.encode_to(&mut encoded_inner);
|
||||
// Encode the actual transaction extensions values
|
||||
self.additional_and_extra_params
|
||||
.encode_value_to(&mut encoded_inner);
|
||||
// and now, call data (remembering that it's been encoded already and just needs appending)
|
||||
encoded_inner.extend(&self.call_data);
|
||||
// now, prefix byte length:
|
||||
let len = Compact(
|
||||
u32::try_from(encoded_inner.len()).expect("extrinsic size expected to be <4GB"),
|
||||
);
|
||||
let mut encoded = Vec::new();
|
||||
len.encode_to(&mut encoded);
|
||||
encoded.extend(encoded_inner);
|
||||
encoded
|
||||
};
|
||||
|
||||
// Return an extrinsic ready to be submitted.
|
||||
Transaction::from_bytes(extrinsic)
|
||||
}
|
||||
|
||||
/// Convert this [`PartialTransactionV5`] into a V5 "general" [`Transaction`] with a signature.
|
||||
///
|
||||
/// Signing the transaction injects the signature into the transaction extension data, which is why
|
||||
/// this method borrows self mutably. Signing repeatedly will override the previous signature.
|
||||
pub fn sign<Signer>(&mut self, signer: &Signer) -> Transaction<T>
|
||||
where
|
||||
Signer: SignerT<T>,
|
||||
{
|
||||
// Given our signer, we can sign the payload representing this extrinsic.
|
||||
let signature = signer.sign(&self.signer_payload());
|
||||
// Now, use the signature and "from" account to build the extrinsic.
|
||||
self.sign_with_account_and_signature(&signer.account_id(), &signature)
|
||||
}
|
||||
|
||||
/// Convert this [`PartialTransactionV5`] into a V5 "general" [`Transaction`] with a signature.
|
||||
/// Prefer [`Self::sign`] if you have a [`SignerT`] instance to use.
|
||||
///
|
||||
/// Signing the transaction injects the signature into the transaction extension data, which is why
|
||||
/// this method borrows self mutably. Signing repeatedly will override the previous signature.
|
||||
pub fn sign_with_account_and_signature(
|
||||
&mut self,
|
||||
account_id: &T::AccountId,
|
||||
signature: &T::Signature,
|
||||
) -> Transaction<T> {
|
||||
// Inject the signature into the transaction extensions
|
||||
// before constructing it.
|
||||
self.additional_and_extra_params
|
||||
.inject_signature(account_id, signature);
|
||||
|
||||
self.to_transaction()
|
||||
}
|
||||
}
|
||||
|
||||
/// This represents a signed transaction that's ready to be submitted.
|
||||
/// Use [`Transaction::encoded()`] or [`Transaction::into_encoded()`] to
|
||||
/// get the bytes for it, or [`Transaction::hash_with()`] to hash the transaction
|
||||
/// given an instance of [`Config::Hasher`].
|
||||
pub struct Transaction<T> {
|
||||
encoded: Encoded,
|
||||
marker: core::marker::PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T: Config> Transaction<T> {
|
||||
/// Create a [`Transaction`] from some already-signed and prepared
|
||||
/// extrinsic bytes,
|
||||
pub fn from_bytes(tx_bytes: Vec<u8>) -> Self {
|
||||
Self {
|
||||
encoded: Encoded(tx_bytes),
|
||||
marker: core::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate and return the hash of the extrinsic, based on the provided hasher.
|
||||
/// If you don't have a hasher to hand, you can construct one using the metadata
|
||||
/// with `T::Hasher::new(&metadata)`. This will create a hasher suitable for the
|
||||
/// current chain where possible.
|
||||
pub fn hash_with(&self, hasher: T::Hasher) -> HashFor<T> {
|
||||
hasher.hash_of(&self.encoded)
|
||||
}
|
||||
|
||||
/// Returns the SCALE encoded extrinsic bytes.
|
||||
pub fn encoded(&self) -> &[u8] {
|
||||
&self.encoded.0
|
||||
}
|
||||
|
||||
/// Consumes this [`Transaction`] and returns the SCALE encoded
|
||||
/// extrinsic bytes.
|
||||
pub fn into_encoded(self) -> Vec<u8> {
|
||||
self.encoded.0
|
||||
}
|
||||
}
|
||||
+279
@@ -0,0 +1,279 @@
|
||||
// Copyright 2019-2024 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! This module contains the trait and types used to represent
|
||||
//! transactions that can be submitted.
|
||||
|
||||
use crate::Metadata;
|
||||
use crate::error::ExtrinsicError;
|
||||
use alloc::borrow::Cow;
|
||||
use alloc::boxed::Box;
|
||||
use alloc::string::{String, ToString};
|
||||
|
||||
use alloc::vec::Vec;
|
||||
use codec::Encode;
|
||||
use scale_encode::EncodeAsFields;
|
||||
use scale_value::{Composite, Value, ValueDef, Variant};
|
||||
|
||||
/// This represents a transaction payload that can be submitted
|
||||
/// to a node.
|
||||
pub trait Payload {
|
||||
/// Encode call data to the provided output.
|
||||
fn encode_call_data_to(
|
||||
&self,
|
||||
metadata: &Metadata,
|
||||
out: &mut Vec<u8>,
|
||||
) -> Result<(), ExtrinsicError>;
|
||||
|
||||
/// Encode call data and return the output. This is a convenience
|
||||
/// wrapper around [`Payload::encode_call_data_to`].
|
||||
fn encode_call_data(&self, metadata: &Metadata) -> Result<Vec<u8>, ExtrinsicError> {
|
||||
let mut v = Vec::new();
|
||||
self.encode_call_data_to(metadata, &mut v)?;
|
||||
Ok(v)
|
||||
}
|
||||
|
||||
/// Returns the details needed to validate the call, which
|
||||
/// include a statically generated hash, the pallet name,
|
||||
/// and the call name.
|
||||
fn validation_details(&self) -> Option<ValidationDetails<'_>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! boxed_payload {
|
||||
($ty:path) => {
|
||||
impl<T: Payload + ?Sized> Payload for $ty {
|
||||
fn encode_call_data_to(
|
||||
&self,
|
||||
metadata: &Metadata,
|
||||
out: &mut Vec<u8>,
|
||||
) -> Result<(), ExtrinsicError> {
|
||||
self.as_ref().encode_call_data_to(metadata, out)
|
||||
}
|
||||
fn encode_call_data(&self, metadata: &Metadata) -> Result<Vec<u8>, ExtrinsicError> {
|
||||
self.as_ref().encode_call_data(metadata)
|
||||
}
|
||||
fn validation_details(&self) -> Option<ValidationDetails<'_>> {
|
||||
self.as_ref().validation_details()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
boxed_payload!(Box<T>);
|
||||
#[cfg(feature = "std")]
|
||||
boxed_payload!(std::sync::Arc<T>);
|
||||
#[cfg(feature = "std")]
|
||||
boxed_payload!(std::rc::Rc<T>);
|
||||
|
||||
/// Details required to validate the shape of a transaction payload against some metadata.
|
||||
pub struct ValidationDetails<'a> {
|
||||
/// The pallet name.
|
||||
pub pallet_name: &'a str,
|
||||
/// The call name.
|
||||
pub call_name: &'a str,
|
||||
/// A hash (this is generated at compile time in our codegen)
|
||||
/// to compare against the runtime code.
|
||||
pub hash: [u8; 32],
|
||||
}
|
||||
|
||||
/// A transaction payload containing some generic `CallData`.
|
||||
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
||||
pub struct DefaultPayload<CallData> {
|
||||
pallet_name: Cow<'static, str>,
|
||||
call_name: Cow<'static, str>,
|
||||
call_data: CallData,
|
||||
validation_hash: Option<[u8; 32]>,
|
||||
}
|
||||
|
||||
/// The payload type used by static codegen.
|
||||
pub type StaticPayload<Calldata> = DefaultPayload<Calldata>;
|
||||
/// The type of a payload typically used for dynamic transaction payloads.
|
||||
pub type DynamicPayload = DefaultPayload<Composite<()>>;
|
||||
|
||||
impl<CallData> DefaultPayload<CallData> {
|
||||
/// Create a new [`DefaultPayload`].
|
||||
pub fn new(
|
||||
pallet_name: impl Into<String>,
|
||||
call_name: impl Into<String>,
|
||||
call_data: CallData,
|
||||
) -> Self {
|
||||
DefaultPayload {
|
||||
pallet_name: Cow::Owned(pallet_name.into()),
|
||||
call_name: Cow::Owned(call_name.into()),
|
||||
call_data,
|
||||
validation_hash: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new [`DefaultPayload`] using static strings for the pallet and call name.
|
||||
/// This is only expected to be used from codegen.
|
||||
#[doc(hidden)]
|
||||
pub fn new_static(
|
||||
pallet_name: &'static str,
|
||||
call_name: &'static str,
|
||||
call_data: CallData,
|
||||
validation_hash: [u8; 32],
|
||||
) -> Self {
|
||||
DefaultPayload {
|
||||
pallet_name: Cow::Borrowed(pallet_name),
|
||||
call_name: Cow::Borrowed(call_name),
|
||||
call_data,
|
||||
validation_hash: Some(validation_hash),
|
||||
}
|
||||
}
|
||||
|
||||
/// Do not validate this call prior to submitting it.
|
||||
pub fn unvalidated(self) -> Self {
|
||||
Self {
|
||||
validation_hash: None,
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the call data.
|
||||
pub fn call_data(&self) -> &CallData {
|
||||
&self.call_data
|
||||
}
|
||||
|
||||
/// Returns the pallet name.
|
||||
pub fn pallet_name(&self) -> &str {
|
||||
&self.pallet_name
|
||||
}
|
||||
|
||||
/// Returns the call name.
|
||||
pub fn call_name(&self) -> &str {
|
||||
&self.call_name
|
||||
}
|
||||
}
|
||||
|
||||
impl DefaultPayload<Composite<()>> {
|
||||
/// Convert the dynamic `Composite` payload into a [`Value`].
|
||||
/// This is useful if you want to use this as an argument for a
|
||||
/// larger dynamic call that wants to use this as a nested call.
|
||||
pub fn into_value(self) -> Value<()> {
|
||||
let call = Value {
|
||||
context: (),
|
||||
value: ValueDef::Variant(Variant {
|
||||
name: self.call_name.into_owned(),
|
||||
values: self.call_data,
|
||||
}),
|
||||
};
|
||||
|
||||
Value::unnamed_variant(self.pallet_name, [call])
|
||||
}
|
||||
}
|
||||
|
||||
impl<CallData: EncodeAsFields> Payload for DefaultPayload<CallData> {
|
||||
fn encode_call_data_to(
|
||||
&self,
|
||||
metadata: &Metadata,
|
||||
out: &mut Vec<u8>,
|
||||
) -> Result<(), ExtrinsicError> {
|
||||
let pallet = metadata
|
||||
.pallet_by_name(&self.pallet_name)
|
||||
.ok_or_else(|| ExtrinsicError::PalletNameNotFound(self.pallet_name.to_string()))?;
|
||||
let call = pallet
|
||||
.call_variant_by_name(&self.call_name)
|
||||
.ok_or_else(|| ExtrinsicError::CallNameNotFound {
|
||||
pallet_name: pallet.name().to_string(),
|
||||
call_name: self.call_name.to_string(),
|
||||
})?;
|
||||
|
||||
let pallet_index = pallet.call_index();
|
||||
let call_index = call.index;
|
||||
|
||||
pallet_index.encode_to(out);
|
||||
call_index.encode_to(out);
|
||||
|
||||
let mut fields = call
|
||||
.fields
|
||||
.iter()
|
||||
.map(|f| scale_encode::Field::new(f.ty.id, f.name.as_deref()));
|
||||
|
||||
self.call_data
|
||||
.encode_as_fields_to(&mut fields, metadata.types(), out)
|
||||
.map_err(ExtrinsicError::CannotEncodeCallData)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validation_details(&self) -> Option<ValidationDetails<'_>> {
|
||||
self.validation_hash.map(|hash| ValidationDetails {
|
||||
pallet_name: &self.pallet_name,
|
||||
call_name: &self.call_name,
|
||||
hash,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Construct a transaction at runtime; essentially an alias to [`DefaultPayload::new()`]
|
||||
/// which provides a [`Composite`] value for the call data.
|
||||
pub fn dynamic(
|
||||
pallet_name: impl Into<String>,
|
||||
call_name: impl Into<String>,
|
||||
call_data: impl Into<Composite<()>>,
|
||||
) -> DynamicPayload {
|
||||
DefaultPayload::new(pallet_name, call_name, call_data.into())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::Metadata;
|
||||
use codec::Decode;
|
||||
use scale_value::Composite;
|
||||
|
||||
fn test_metadata() -> Metadata {
|
||||
let metadata_bytes = include_bytes!("../../../artifacts/polkadot_metadata_small.scale");
|
||||
Metadata::decode(&mut &metadata_bytes[..]).expect("Valid metadata")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encode_call_with_incompatible_types_returns_error() {
|
||||
let metadata = test_metadata();
|
||||
|
||||
let incompatible_data = Composite::named([
|
||||
("dest", scale_value::Value::bool(true)), // Boolean instead of MultiAddress
|
||||
("value", scale_value::Value::string("not_a_number")), // String instead of u128
|
||||
]);
|
||||
|
||||
let payload = DefaultPayload::new("Balances", "transfer_allow_death", incompatible_data);
|
||||
|
||||
let mut out = Vec::new();
|
||||
let result = payload.encode_call_data_to(&metadata, &mut out);
|
||||
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"Expected error when encoding with incompatible types"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encode_call_with_valid_data_succeeds() {
|
||||
let metadata = test_metadata();
|
||||
|
||||
// Create a valid payload to ensure our error handling doesn't break valid cases
|
||||
// For MultiAddress, we'll use the Id variant with a 32-byte account
|
||||
let valid_address =
|
||||
scale_value::Value::unnamed_variant("Id", [scale_value::Value::from_bytes([0u8; 32])]);
|
||||
|
||||
let valid_data = Composite::named([
|
||||
("dest", valid_address),
|
||||
("value", scale_value::Value::u128(1000)),
|
||||
]);
|
||||
|
||||
let payload = DefaultPayload::new("Balances", "transfer_allow_death", valid_data);
|
||||
|
||||
// This should succeed
|
||||
let mut out = Vec::new();
|
||||
let result = payload.encode_call_data_to(&metadata, &mut out);
|
||||
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"Expected success when encoding with valid data"
|
||||
);
|
||||
assert!(!out.is_empty(), "Expected encoded output to be non-empty");
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
// Copyright 2019-2024 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! A library to **sub**mit e**xt**rinsics to a
|
||||
//! [substrate](https://github.com/paritytech/substrate) node via RPC.
|
||||
|
||||
use crate::Config;
|
||||
|
||||
/// Signing transactions requires a [`Signer`]. This is responsible for
|
||||
/// providing the "from" account that the transaction is being signed by,
|
||||
/// as well as actually signing a SCALE encoded payload.
|
||||
pub trait Signer<T: Config> {
|
||||
/// Return the "from" account ID.
|
||||
fn account_id(&self) -> T::AccountId;
|
||||
|
||||
/// Takes a signer payload for an extrinsic, and returns a signature based on it.
|
||||
///
|
||||
/// Some signers may fail, for instance because the hardware on which the keys are located has
|
||||
/// refused the operation.
|
||||
fn sign(&self, signer_payload: &[u8]) -> T::Signature;
|
||||
}
|
||||
+192
@@ -0,0 +1,192 @@
|
||||
// Copyright 2019-2024 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! The "default" Substrate/Polkadot AccountId. This is used in codegen, as well as signing related bits.
|
||||
//! This doesn't contain much functionality itself, but is easy to convert to/from an `sp_core::AccountId32`
|
||||
//! for instance, to gain functionality without forcing a dependency on Substrate crates here.
|
||||
|
||||
use alloc::format;
|
||||
use alloc::string::String;
|
||||
use alloc::vec;
|
||||
use alloc::vec::Vec;
|
||||
use codec::{Decode, Encode};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error as DeriveError;
|
||||
|
||||
/// A 32-byte cryptographic identifier. This is a simplified version of Substrate's
|
||||
/// `sp_core::crypto::AccountId32`. To obtain more functionality, convert this into
|
||||
/// that type.
|
||||
#[derive(
|
||||
Clone,
|
||||
Eq,
|
||||
PartialEq,
|
||||
Ord,
|
||||
PartialOrd,
|
||||
Encode,
|
||||
Decode,
|
||||
Debug,
|
||||
scale_encode::EncodeAsType,
|
||||
scale_decode::DecodeAsType,
|
||||
scale_info::TypeInfo,
|
||||
)]
|
||||
pub struct AccountId32(pub [u8; 32]);
|
||||
|
||||
impl AsRef<[u8]> for AccountId32 {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
&self.0[..]
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[u8; 32]> for AccountId32 {
|
||||
fn as_ref(&self) -> &[u8; 32] {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<[u8; 32]> for AccountId32 {
|
||||
fn from(x: [u8; 32]) -> Self {
|
||||
AccountId32(x)
|
||||
}
|
||||
}
|
||||
|
||||
impl AccountId32 {
|
||||
// Return the ss58-check string for this key. Adapted from `sp_core::crypto`. We need this to
|
||||
// serialize our account appropriately but otherwise don't care.
|
||||
fn to_ss58check(&self) -> String {
|
||||
// For serializing to a string to obtain the account nonce, we use the default substrate
|
||||
// prefix (since we have no way to otherwise pick one). It doesn't really matter, since when
|
||||
// it's deserialized back in system_accountNextIndex, we ignore this (so long as it's valid).
|
||||
const SUBSTRATE_SS58_PREFIX: u8 = 42;
|
||||
// prefix <= 63 just take up one byte at the start:
|
||||
let mut v = vec![SUBSTRATE_SS58_PREFIX];
|
||||
// then push the account ID bytes.
|
||||
v.extend(self.0);
|
||||
// then push a 2 byte checksum of what we have so far.
|
||||
let r = ss58hash(&v);
|
||||
v.extend(&r[0..2]);
|
||||
// then encode to base58.
|
||||
use base58::ToBase58;
|
||||
v.to_base58()
|
||||
}
|
||||
|
||||
// This isn't strictly needed, but to give our AccountId32 a little more usefulness, we also
|
||||
// implement the logic needed to decode an AccountId32 from an SS58 encoded string. This is exposed
|
||||
// via a `FromStr` impl.
|
||||
fn from_ss58check(s: &str) -> Result<Self, FromSs58Error> {
|
||||
const CHECKSUM_LEN: usize = 2;
|
||||
let body_len = 32;
|
||||
|
||||
use base58::FromBase58;
|
||||
let data = s.from_base58().map_err(|_| FromSs58Error::BadBase58)?;
|
||||
if data.len() < 2 {
|
||||
return Err(FromSs58Error::BadLength);
|
||||
}
|
||||
let prefix_len = match data[0] {
|
||||
0..=63 => 1,
|
||||
64..=127 => 2,
|
||||
_ => return Err(FromSs58Error::InvalidPrefix),
|
||||
};
|
||||
if data.len() != prefix_len + body_len + CHECKSUM_LEN {
|
||||
return Err(FromSs58Error::BadLength);
|
||||
}
|
||||
let hash = ss58hash(&data[0..body_len + prefix_len]);
|
||||
let checksum = &hash[0..CHECKSUM_LEN];
|
||||
if data[body_len + prefix_len..body_len + prefix_len + CHECKSUM_LEN] != *checksum {
|
||||
// Invalid checksum.
|
||||
return Err(FromSs58Error::InvalidChecksum);
|
||||
}
|
||||
|
||||
let result = data[prefix_len..body_len + prefix_len]
|
||||
.try_into()
|
||||
.map_err(|_| FromSs58Error::BadLength)?;
|
||||
Ok(AccountId32(result))
|
||||
}
|
||||
}
|
||||
|
||||
/// An error obtained from trying to interpret an SS58 encoded string into an AccountId32
|
||||
#[derive(Clone, Copy, Eq, PartialEq, Debug, DeriveError)]
|
||||
#[allow(missing_docs)]
|
||||
pub enum FromSs58Error {
|
||||
#[error("Base 58 requirement is violated")]
|
||||
BadBase58,
|
||||
#[error("Length is bad")]
|
||||
BadLength,
|
||||
#[error("Invalid checksum")]
|
||||
InvalidChecksum,
|
||||
#[error("Invalid SS58 prefix byte.")]
|
||||
InvalidPrefix,
|
||||
}
|
||||
|
||||
// We do this just to get a checksum to help verify the validity of the address in to_ss58check
|
||||
fn ss58hash(data: &[u8]) -> Vec<u8> {
|
||||
use blake2::{Blake2b512, Digest};
|
||||
const PREFIX: &[u8] = b"SS58PRE";
|
||||
let mut ctx = Blake2b512::new();
|
||||
ctx.update(PREFIX);
|
||||
ctx.update(data);
|
||||
ctx.finalize().to_vec()
|
||||
}
|
||||
|
||||
impl Serialize for AccountId32 {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(&self.to_ss58check())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for AccountId32 {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
AccountId32::from_ss58check(&String::deserialize(deserializer)?)
|
||||
.map_err(|e| serde::de::Error::custom(format!("{e:?}")))
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for AccountId32 {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
write!(f, "{}", self.to_ss58check())
|
||||
}
|
||||
}
|
||||
|
||||
impl core::str::FromStr for AccountId32 {
|
||||
type Err = FromSs58Error;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
AccountId32::from_ss58check(s)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use sp_core::{self, crypto::Ss58Codec};
|
||||
use sp_keyring::sr25519::Keyring;
|
||||
|
||||
#[test]
|
||||
fn ss58_is_compatible_with_substrate_impl() {
|
||||
let keyrings = vec![Keyring::Alice, Keyring::Bob, Keyring::Charlie];
|
||||
|
||||
for keyring in keyrings {
|
||||
let substrate_account = keyring.to_account_id();
|
||||
let local_account = AccountId32(substrate_account.clone().into());
|
||||
|
||||
// Both should encode to ss58 the same way:
|
||||
let substrate_ss58 = substrate_account.to_ss58check();
|
||||
assert_eq!(substrate_ss58, local_account.to_ss58check());
|
||||
|
||||
// Both should decode from ss58 back to the same:
|
||||
assert_eq!(
|
||||
sp_core::crypto::AccountId32::from_ss58check(&substrate_ss58).unwrap(),
|
||||
substrate_account
|
||||
);
|
||||
assert_eq!(
|
||||
AccountId32::from_ss58check(&substrate_ss58).unwrap(),
|
||||
local_account
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
// Copyright 2019-2024 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! `AccountId20` is a representation of Ethereum address derived from hashing the public key.
|
||||
|
||||
use alloc::format;
|
||||
use alloc::string::String;
|
||||
use codec::{Decode, Encode};
|
||||
use keccak_hash::keccak;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error as DeriveError;
|
||||
|
||||
#[derive(
|
||||
Copy,
|
||||
Clone,
|
||||
Eq,
|
||||
PartialEq,
|
||||
Ord,
|
||||
PartialOrd,
|
||||
Encode,
|
||||
Decode,
|
||||
Debug,
|
||||
scale_encode::EncodeAsType,
|
||||
scale_decode::DecodeAsType,
|
||||
scale_info::TypeInfo,
|
||||
)]
|
||||
/// Ethereum-compatible `AccountId`.
|
||||
pub struct AccountId20(pub [u8; 20]);
|
||||
|
||||
impl AsRef<[u8]> for AccountId20 {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
&self.0[..]
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[u8; 20]> for AccountId20 {
|
||||
fn as_ref(&self) -> &[u8; 20] {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<[u8; 20]> for AccountId20 {
|
||||
fn from(x: [u8; 20]) -> Self {
|
||||
AccountId20(x)
|
||||
}
|
||||
}
|
||||
|
||||
impl AccountId20 {
|
||||
/// Convert to a public key hash
|
||||
pub fn checksum(&self) -> String {
|
||||
let hex_address = hex::encode(self.0);
|
||||
let hash = keccak(hex_address.as_bytes());
|
||||
|
||||
let mut checksum_address = String::with_capacity(42);
|
||||
checksum_address.push_str("0x");
|
||||
|
||||
for (i, ch) in hex_address.chars().enumerate() {
|
||||
// Get the corresponding nibble from the hash
|
||||
let nibble = (hash[i / 2] >> (if i % 2 == 0 { 4 } else { 0 })) & 0xf;
|
||||
|
||||
if nibble >= 8 {
|
||||
checksum_address.push(ch.to_ascii_uppercase());
|
||||
} else {
|
||||
checksum_address.push(ch);
|
||||
}
|
||||
}
|
||||
|
||||
checksum_address
|
||||
}
|
||||
}
|
||||
|
||||
/// An error obtained from trying to interpret a hex encoded string into an AccountId20
|
||||
#[derive(Clone, Copy, Eq, PartialEq, Debug, DeriveError)]
|
||||
#[allow(missing_docs)]
|
||||
pub enum FromChecksumError {
|
||||
#[error("Length is bad")]
|
||||
BadLength,
|
||||
#[error("Invalid checksum")]
|
||||
InvalidChecksum,
|
||||
#[error("Invalid checksum prefix byte.")]
|
||||
InvalidPrefix,
|
||||
}
|
||||
|
||||
impl Serialize for AccountId20 {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(&self.checksum())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for AccountId20 {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
String::deserialize(deserializer)?
|
||||
.parse::<AccountId20>()
|
||||
.map_err(|e| serde::de::Error::custom(format!("{e:?}")))
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for AccountId20 {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
write!(f, "{}", self.checksum())
|
||||
}
|
||||
}
|
||||
|
||||
impl core::str::FromStr for AccountId20 {
|
||||
type Err = FromChecksumError;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
if s.len() != 42 {
|
||||
return Err(FromChecksumError::BadLength);
|
||||
}
|
||||
if !s.starts_with("0x") {
|
||||
return Err(FromChecksumError::InvalidPrefix);
|
||||
}
|
||||
hex::decode(&s.as_bytes()[2..])
|
||||
.map_err(|_| FromChecksumError::InvalidChecksum)?
|
||||
.try_into()
|
||||
.map(AccountId20)
|
||||
.map_err(|_| FromChecksumError::BadLength)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn deserialisation() {
|
||||
let key_hashes = vec![
|
||||
"0xf24FF3a9CF04c71Dbc94D0b566f7A27B94566cac",
|
||||
"0x3Cd0A705a2DC65e5b1E1205896BaA2be8A07c6e0",
|
||||
"0x798d4Ba9baf0064Ec19eB4F0a1a45785ae9D6DFc",
|
||||
"0x773539d4Ac0e786233D90A233654ccEE26a613D9",
|
||||
"0xFf64d3F6efE2317EE2807d223a0Bdc4c0c49dfDB",
|
||||
"0xC0F0f4ab324C46e55D02D0033343B4Be8A55532d",
|
||||
];
|
||||
|
||||
for key_hash in key_hashes {
|
||||
let parsed: AccountId20 = key_hash.parse().expect("Failed to parse");
|
||||
|
||||
let encoded = parsed.checksum();
|
||||
|
||||
// `encoded` should be equal to the initial key_hash
|
||||
assert_eq!(encoded, key_hash);
|
||||
}
|
||||
}
|
||||
}
|
||||
+266
@@ -0,0 +1,266 @@
|
||||
// Copyright 2019-2024 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! Generic `scale_bits` over `bitvec`-like `BitOrder` and `BitFormat` types.
|
||||
|
||||
use alloc::vec;
|
||||
use alloc::vec::Vec;
|
||||
use codec::{Compact, Input};
|
||||
use core::marker::PhantomData;
|
||||
use scale_bits::{
|
||||
Bits,
|
||||
scale::format::{Format, OrderFormat, StoreFormat},
|
||||
};
|
||||
use scale_decode::{IntoVisitor, TypeResolver};
|
||||
|
||||
/// Associates `bitvec::store::BitStore` trait with corresponding, type-erased `scale_bits::StoreFormat` enum.
|
||||
///
|
||||
/// Used to decode bit sequences by providing `scale_bits::StoreFormat` using
|
||||
/// `bitvec`-like type type parameters.
|
||||
pub trait BitStore {
|
||||
/// Corresponding `scale_bits::StoreFormat` value.
|
||||
const FORMAT: StoreFormat;
|
||||
/// Number of bits that the backing store types holds.
|
||||
const BITS: u32;
|
||||
}
|
||||
macro_rules! impl_store {
|
||||
($ty:ident, $wrapped:ty) => {
|
||||
impl BitStore for $wrapped {
|
||||
const FORMAT: StoreFormat = StoreFormat::$ty;
|
||||
const BITS: u32 = <$wrapped>::BITS;
|
||||
}
|
||||
};
|
||||
}
|
||||
impl_store!(U8, u8);
|
||||
impl_store!(U16, u16);
|
||||
impl_store!(U32, u32);
|
||||
impl_store!(U64, u64);
|
||||
|
||||
/// Associates `bitvec::order::BitOrder` trait with corresponding, type-erased `scale_bits::OrderFormat` enum.
|
||||
///
|
||||
/// Used to decode bit sequences in runtime by providing `scale_bits::OrderFormat` using
|
||||
/// `bitvec`-like type type parameters.
|
||||
pub trait BitOrder {
|
||||
/// Corresponding `scale_bits::OrderFormat` value.
|
||||
const FORMAT: OrderFormat;
|
||||
}
|
||||
macro_rules! impl_order {
|
||||
($ty:ident) => {
|
||||
#[doc = concat!("Type-level value that corresponds to `scale_bits::OrderFormat::", stringify!($ty), "` at run-time")]
|
||||
#[doc = concat!(" and `bitvec::order::BitOrder::", stringify!($ty), "` at the type level.")]
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum $ty {}
|
||||
impl BitOrder for $ty {
|
||||
const FORMAT: OrderFormat = OrderFormat::$ty;
|
||||
}
|
||||
};
|
||||
}
|
||||
impl_order!(Lsb0);
|
||||
impl_order!(Msb0);
|
||||
|
||||
/// Constructs a run-time format parameters based on the corresponding type-level parameters.
|
||||
fn bit_format<Store: BitStore, Order: BitOrder>() -> Format {
|
||||
Format {
|
||||
order: Order::FORMAT,
|
||||
store: Store::FORMAT,
|
||||
}
|
||||
}
|
||||
|
||||
/// `scale_bits::Bits` generic over the bit store (`u8`/`u16`/`u32`/`u64`) and bit order (LSB, MSB)
|
||||
/// used for SCALE encoding/decoding. Uses `scale_bits::Bits`-default `u8` and LSB format underneath.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct DecodedBits<Store, Order> {
|
||||
bits: Bits,
|
||||
_marker: PhantomData<(Store, Order)>,
|
||||
}
|
||||
|
||||
impl<Store, Order> DecodedBits<Store, Order> {
|
||||
/// Extracts the underlying `scale_bits::Bits` value.
|
||||
pub fn into_bits(self) -> Bits {
|
||||
self.bits
|
||||
}
|
||||
|
||||
/// References the underlying `scale_bits::Bits` value.
|
||||
pub fn as_bits(&self) -> &Bits {
|
||||
&self.bits
|
||||
}
|
||||
}
|
||||
|
||||
impl<Store, Order> core::iter::FromIterator<bool> for DecodedBits<Store, Order> {
|
||||
fn from_iter<T: IntoIterator<Item = bool>>(iter: T) -> Self {
|
||||
DecodedBits {
|
||||
bits: Bits::from_iter(iter),
|
||||
_marker: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Store: BitStore, Order: BitOrder> codec::Decode for DecodedBits<Store, Order> {
|
||||
fn decode<I: Input>(input: &mut I) -> Result<Self, codec::Error> {
|
||||
/// Equivalent of `BitSlice::MAX_BITS` on 32bit machine.
|
||||
const ARCH32BIT_BITSLICE_MAX_BITS: u32 = 0x1fff_ffff;
|
||||
|
||||
let Compact(bits) = <Compact<u32>>::decode(input)?;
|
||||
// Otherwise it is impossible to store it on 32bit machine.
|
||||
if bits > ARCH32BIT_BITSLICE_MAX_BITS {
|
||||
return Err("Attempt to decode a BitVec with too many bits".into());
|
||||
}
|
||||
// NOTE: Replace with `bits.div_ceil(Store::BITS)` if `int_roundings` is stabilised
|
||||
let elements = (bits / Store::BITS) + u32::from(bits % Store::BITS != 0);
|
||||
let bytes_in_elem = Store::BITS.saturating_div(u8::BITS);
|
||||
let bytes_needed = (elements * bytes_in_elem) as usize;
|
||||
|
||||
// NOTE: We could reduce allocations if it would be possible to directly
|
||||
// decode from an `Input` type using a custom format (rather than default <u8, Lsb0>)
|
||||
// for the `Bits` type.
|
||||
let mut storage = codec::Encode::encode(&Compact(bits));
|
||||
let prefix_len = storage.len();
|
||||
storage.reserve_exact(bytes_needed);
|
||||
storage.extend(vec![0; bytes_needed]);
|
||||
input.read(&mut storage[prefix_len..])?;
|
||||
|
||||
let decoder = scale_bits::decode_using_format_from(&storage, bit_format::<Store, Order>())?;
|
||||
let bits = decoder.collect::<Result<Vec<_>, _>>()?;
|
||||
let bits = Bits::from_iter(bits);
|
||||
|
||||
Ok(DecodedBits {
|
||||
bits,
|
||||
_marker: PhantomData,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<Store: BitStore, Order: BitOrder> codec::Encode for DecodedBits<Store, Order> {
|
||||
fn size_hint(&self) -> usize {
|
||||
self.bits.size_hint()
|
||||
}
|
||||
|
||||
fn encoded_size(&self) -> usize {
|
||||
self.bits.encoded_size()
|
||||
}
|
||||
|
||||
fn encode(&self) -> Vec<u8> {
|
||||
scale_bits::encode_using_format(self.bits.iter(), bit_format::<Store, Order>())
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub struct DecodedBitsVisitor<S, O, R: TypeResolver>(core::marker::PhantomData<(S, O, R)>);
|
||||
|
||||
impl<Store, Order, R: TypeResolver> scale_decode::Visitor for DecodedBitsVisitor<Store, Order, R> {
|
||||
type Value<'scale, 'info> = DecodedBits<Store, Order>;
|
||||
type Error = scale_decode::Error;
|
||||
type TypeResolver = R;
|
||||
|
||||
fn unchecked_decode_as_type<'scale, 'info>(
|
||||
self,
|
||||
input: &mut &'scale [u8],
|
||||
type_id: R::TypeId,
|
||||
types: &'info R,
|
||||
) -> scale_decode::visitor::DecodeAsTypeResult<
|
||||
Self,
|
||||
Result<Self::Value<'scale, 'info>, Self::Error>,
|
||||
> {
|
||||
let res =
|
||||
scale_decode::visitor::decode_with_visitor(input, type_id, types, Bits::into_visitor())
|
||||
.map(|bits| DecodedBits {
|
||||
bits,
|
||||
_marker: PhantomData,
|
||||
});
|
||||
scale_decode::visitor::DecodeAsTypeResult::Decoded(res)
|
||||
}
|
||||
}
|
||||
impl<Store, Order> scale_decode::IntoVisitor for DecodedBits<Store, Order> {
|
||||
type AnyVisitor<R: scale_decode::TypeResolver> = DecodedBitsVisitor<Store, Order, R>;
|
||||
fn into_visitor<R: TypeResolver>() -> DecodedBitsVisitor<Store, Order, R> {
|
||||
DecodedBitsVisitor(PhantomData)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Store, Order> scale_encode::EncodeAsType for DecodedBits<Store, Order> {
|
||||
fn encode_as_type_to<R: TypeResolver>(
|
||||
&self,
|
||||
type_id: R::TypeId,
|
||||
types: &R,
|
||||
out: &mut Vec<u8>,
|
||||
) -> Result<(), scale_encode::Error> {
|
||||
self.bits.encode_as_type_to(type_id, types, out)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use core::fmt::Debug;
|
||||
|
||||
use bitvec::vec::BitVec;
|
||||
use codec::Decode as _;
|
||||
|
||||
// NOTE: We don't use `bitvec::order` types in our implementation, since we
|
||||
// don't want to depend on `bitvec`. Rather than reimplementing the unsafe
|
||||
// trait on our types here for testing purposes, we simply convert and
|
||||
// delegate to `bitvec`'s own types.
|
||||
trait ToBitVec {
|
||||
type Order: bitvec::order::BitOrder;
|
||||
}
|
||||
impl ToBitVec for Lsb0 {
|
||||
type Order = bitvec::order::Lsb0;
|
||||
}
|
||||
impl ToBitVec for Msb0 {
|
||||
type Order = bitvec::order::Msb0;
|
||||
}
|
||||
|
||||
fn scales_like_bitvec_and_roundtrips<
|
||||
'a,
|
||||
Store: BitStore + bitvec::store::BitStore + PartialEq,
|
||||
Order: BitOrder + ToBitVec + Debug + PartialEq,
|
||||
>(
|
||||
input: impl IntoIterator<Item = &'a bool>,
|
||||
) where
|
||||
BitVec<Store, <Order as ToBitVec>::Order>: codec::Encode + codec::Decode,
|
||||
{
|
||||
let input: Vec<_> = input.into_iter().copied().collect();
|
||||
|
||||
let decoded_bits = DecodedBits::<Store, Order>::from_iter(input.clone());
|
||||
let bitvec = BitVec::<Store, <Order as ToBitVec>::Order>::from_iter(input);
|
||||
|
||||
let decoded_bits_encoded = codec::Encode::encode(&decoded_bits);
|
||||
let bitvec_encoded = codec::Encode::encode(&bitvec);
|
||||
assert_eq!(decoded_bits_encoded, bitvec_encoded);
|
||||
|
||||
let decoded_bits_decoded =
|
||||
DecodedBits::<Store, Order>::decode(&mut &decoded_bits_encoded[..])
|
||||
.expect("SCALE-encoding DecodedBits to roundtrip");
|
||||
let bitvec_decoded =
|
||||
BitVec::<Store, <Order as ToBitVec>::Order>::decode(&mut &bitvec_encoded[..])
|
||||
.expect("SCALE-encoding BitVec to roundtrip");
|
||||
assert_eq!(decoded_bits, decoded_bits_decoded);
|
||||
assert_eq!(bitvec, bitvec_decoded);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decoded_bitvec_scales_and_roundtrips() {
|
||||
let test_cases = [
|
||||
vec![],
|
||||
vec![true],
|
||||
vec![false],
|
||||
vec![true, false, true],
|
||||
vec![true, false, true, false, false, false, false, false, true],
|
||||
[vec![true; 5], vec![false; 5], vec![true; 1], vec![false; 3]].concat(),
|
||||
[vec![true; 9], vec![false; 9], vec![true; 9], vec![false; 9]].concat(),
|
||||
];
|
||||
|
||||
for test_case in &test_cases {
|
||||
scales_like_bitvec_and_roundtrips::<u8, Lsb0>(test_case);
|
||||
scales_like_bitvec_and_roundtrips::<u16, Lsb0>(test_case);
|
||||
scales_like_bitvec_and_roundtrips::<u32, Lsb0>(test_case);
|
||||
scales_like_bitvec_and_roundtrips::<u64, Lsb0>(test_case);
|
||||
scales_like_bitvec_and_roundtrips::<u8, Msb0>(test_case);
|
||||
scales_like_bitvec_and_roundtrips::<u16, Msb0>(test_case);
|
||||
scales_like_bitvec_and_roundtrips::<u32, Msb0>(test_case);
|
||||
scales_like_bitvec_and_roundtrips::<u64, Msb0>(test_case);
|
||||
}
|
||||
}
|
||||
}
|
||||
+234
@@ -0,0 +1,234 @@
|
||||
// Copyright 2019-2024 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
use alloc::{format, vec::Vec};
|
||||
use codec::{Decode, Encode};
|
||||
use scale_decode::{
|
||||
IntoVisitor, TypeResolver, Visitor,
|
||||
ext::scale_type_resolver,
|
||||
visitor::{TypeIdFor, types::Composite, types::Variant},
|
||||
};
|
||||
use scale_encode::EncodeAsType;
|
||||
|
||||
// Dev note: This and related bits taken from `sp_runtime::generic::Era`
|
||||
/// An era to describe the longevity of a transaction.
|
||||
#[derive(
|
||||
PartialEq,
|
||||
Default,
|
||||
Eq,
|
||||
Clone,
|
||||
Copy,
|
||||
Debug,
|
||||
serde::Serialize,
|
||||
serde::Deserialize,
|
||||
scale_info::TypeInfo,
|
||||
)]
|
||||
pub enum Era {
|
||||
/// The transaction is valid forever. The genesis hash must be present in the signed content.
|
||||
#[default]
|
||||
Immortal,
|
||||
|
||||
/// The transaction will expire. Use [`Era::mortal`] to construct this with correct values.
|
||||
///
|
||||
/// When used on `FRAME`-based runtimes, `period` cannot exceed `BlockHashCount` parameter
|
||||
/// of `system` module.
|
||||
Mortal {
|
||||
/// The number of blocks that the tx will be valid for after the checkpoint block
|
||||
/// hash found in the signer payload.
|
||||
period: u64,
|
||||
/// The phase in the period that this transaction's lifetime begins (and, importantly,
|
||||
/// implies which block hash is included in the signature material). If the `period` is
|
||||
/// greater than 1 << 12, then it will be a factor of the times greater than 1<<12 that
|
||||
/// `period` is.
|
||||
phase: u64,
|
||||
},
|
||||
}
|
||||
|
||||
// E.g. with period == 4:
|
||||
// 0 10 20 30 40
|
||||
// 0123456789012345678901234567890123456789012
|
||||
// |...|
|
||||
// authored -/ \- expiry
|
||||
// phase = 1
|
||||
// n = Q(current - phase, period) + phase
|
||||
impl Era {
|
||||
/// Create a new era based on a period (which should be a power of two between 4 and 65536
|
||||
/// inclusive) and a block number on which it should start (or, for long periods, be shortly
|
||||
/// after the start).
|
||||
///
|
||||
/// If using `Era` in the context of `FRAME` runtime, make sure that `period`
|
||||
/// does not exceed `BlockHashCount` parameter passed to `system` module, since that
|
||||
/// prunes old blocks and renders transactions immediately invalid.
|
||||
pub fn mortal(period: u64, current: u64) -> Self {
|
||||
let period = period
|
||||
.checked_next_power_of_two()
|
||||
.unwrap_or(1 << 16)
|
||||
.clamp(4, 1 << 16);
|
||||
let phase = current % period;
|
||||
let quantize_factor = (period >> 12).max(1);
|
||||
let quantized_phase = phase / quantize_factor * quantize_factor;
|
||||
|
||||
Self::Mortal {
|
||||
period,
|
||||
phase: quantized_phase,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Both copied from `sp_runtime::generic::Era`; this is the wire interface and so
|
||||
// it's really the most important bit here.
|
||||
impl codec::Encode for Era {
|
||||
fn encode_to<T: codec::Output + ?Sized>(&self, output: &mut T) {
|
||||
match self {
|
||||
Self::Immortal => output.push_byte(0),
|
||||
Self::Mortal { period, phase } => {
|
||||
let quantize_factor = (*period >> 12).max(1);
|
||||
let encoded = (period.trailing_zeros() - 1).clamp(1, 15) as u16
|
||||
| ((phase / quantize_factor) << 4) as u16;
|
||||
encoded.encode_to(output);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
impl codec::Decode for Era {
|
||||
fn decode<I: codec::Input>(input: &mut I) -> Result<Self, codec::Error> {
|
||||
let first = input.read_byte()?;
|
||||
if first == 0 {
|
||||
Ok(Self::Immortal)
|
||||
} else {
|
||||
let encoded = first as u64 + ((input.read_byte()? as u64) << 8);
|
||||
let period = 2 << (encoded % (1 << 4));
|
||||
let quantize_factor = (period >> 12).max(1);
|
||||
let phase = (encoded >> 4) * quantize_factor;
|
||||
if period >= 4 && phase < period {
|
||||
Ok(Self::Mortal { period, phase })
|
||||
} else {
|
||||
Err("Invalid period and phase".into())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Define manually how to encode an Era given some type information. Here we
|
||||
/// basically check that the type we're targeting is called "Era" and then codec::Encode.
|
||||
impl EncodeAsType for Era {
|
||||
fn encode_as_type_to<R: TypeResolver>(
|
||||
&self,
|
||||
type_id: R::TypeId,
|
||||
types: &R,
|
||||
out: &mut Vec<u8>,
|
||||
) -> Result<(), scale_encode::Error> {
|
||||
// Visit the type to check that it is an Era. This is only a rough check.
|
||||
let visitor = scale_type_resolver::visitor::new((), |_, _| false)
|
||||
.visit_variant(|_, path, _variants| path.last() == Some("Era"));
|
||||
|
||||
let is_era = types
|
||||
.resolve_type(type_id.clone(), visitor)
|
||||
.unwrap_or_default();
|
||||
if !is_era {
|
||||
return Err(scale_encode::Error::custom_string(format!(
|
||||
"Type {type_id:?} is not a valid Era type; expecting either Immortal or MortalX variant"
|
||||
)));
|
||||
}
|
||||
|
||||
// if the type looks valid then just scale encode our Era.
|
||||
self.encode_to(out);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Define manually how to decode an Era given some type information. Here we check that the
|
||||
/// variant we're decoding is one of the expected Era variants, and that the field is correct if so,
|
||||
/// ensuring that this will fail if trying to decode something that isn't an Era.
|
||||
pub struct EraVisitor<R>(core::marker::PhantomData<R>);
|
||||
|
||||
impl IntoVisitor for Era {
|
||||
type AnyVisitor<R: TypeResolver> = EraVisitor<R>;
|
||||
fn into_visitor<R: TypeResolver>() -> Self::AnyVisitor<R> {
|
||||
EraVisitor(core::marker::PhantomData)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: TypeResolver> Visitor for EraVisitor<R> {
|
||||
type Value<'scale, 'resolver> = Era;
|
||||
type Error = scale_decode::Error;
|
||||
type TypeResolver = R;
|
||||
|
||||
// Unwrap any newtype wrappers around the era, eg the CheckMortality extension (which actually
|
||||
// has 2 fields, but scale_info seems to automatically ignore the PhantomData field). This
|
||||
// allows us to decode directly from CheckMortality into Era.
|
||||
fn visit_composite<'scale, 'resolver>(
|
||||
self,
|
||||
value: &mut Composite<'scale, 'resolver, Self::TypeResolver>,
|
||||
_type_id: TypeIdFor<Self>,
|
||||
) -> Result<Self::Value<'scale, 'resolver>, Self::Error> {
|
||||
if value.remaining() != 1 {
|
||||
return Err(scale_decode::Error::custom_string(format!(
|
||||
"Expected any wrapper around Era to have exactly one field, but got {} fields",
|
||||
value.remaining()
|
||||
)));
|
||||
}
|
||||
|
||||
value
|
||||
.decode_item(self)
|
||||
.expect("1 field expected; checked above.")
|
||||
}
|
||||
|
||||
fn visit_variant<'scale, 'resolver>(
|
||||
self,
|
||||
value: &mut Variant<'scale, 'resolver, Self::TypeResolver>,
|
||||
_type_id: TypeIdFor<Self>,
|
||||
) -> Result<Self::Value<'scale, 'resolver>, Self::Error> {
|
||||
let variant = value.name();
|
||||
|
||||
// If the variant is immortal, we know the outcome.
|
||||
if variant == "Immortal" {
|
||||
return Ok(Era::Immortal);
|
||||
}
|
||||
|
||||
// Otherwise, we expect a variant Mortal1..Mortal255 where the number
|
||||
// here is the first byte, and the second byte is conceptually a field of this variant.
|
||||
// This weird encoding is because the Era is compressed to just 1 byte if immortal and
|
||||
// just 2 bytes if mortal.
|
||||
//
|
||||
// Note: We _could_ just assume we'll have 2 bytes to work with and decode the era directly,
|
||||
// but checking the variant names ensures that the thing we think is an Era actually _is_
|
||||
// one, based on the type info for it.
|
||||
let first_byte = variant
|
||||
.strip_prefix("Mortal")
|
||||
.and_then(|s| s.parse::<u8>().ok())
|
||||
.ok_or_else(|| {
|
||||
scale_decode::Error::custom_string(format!(
|
||||
"Expected MortalX variant, but got {variant}"
|
||||
))
|
||||
})?;
|
||||
|
||||
// We need 1 field in the MortalN variant containing the second byte.
|
||||
let mortal_fields = value.fields();
|
||||
if mortal_fields.remaining() != 1 {
|
||||
return Err(scale_decode::Error::custom_string(format!(
|
||||
"Expected Mortal{} to have one u8 field, but got {} fields",
|
||||
first_byte,
|
||||
mortal_fields.remaining()
|
||||
)));
|
||||
}
|
||||
|
||||
let second_byte = mortal_fields
|
||||
.decode_item(u8::into_visitor())
|
||||
.expect("At least one field should exist; checked above.")
|
||||
.map_err(|e| {
|
||||
scale_decode::Error::custom_string(format!(
|
||||
"Expected mortal variant field to be u8, but: {e}"
|
||||
))
|
||||
})?;
|
||||
|
||||
// Now that we have both bytes we can decode them into the era using
|
||||
// the same logic as the codec::Decode impl does.
|
||||
Era::decode(&mut &[first_byte, second_byte][..]).map_err(|e| {
|
||||
scale_decode::Error::custom_string(format!(
|
||||
"Failed to codec::Decode Era from Mortal bytes: {e}"
|
||||
))
|
||||
})
|
||||
}
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
// Copyright 2019-2024 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! Miscellaneous utility helpers.
|
||||
|
||||
mod account_id;
|
||||
mod account_id20;
|
||||
pub mod bits;
|
||||
mod era;
|
||||
mod multi_address;
|
||||
mod multi_signature;
|
||||
mod static_type;
|
||||
mod unchecked_extrinsic;
|
||||
mod wrapper_opaque;
|
||||
mod yesnomaybe;
|
||||
|
||||
use alloc::borrow::ToOwned;
|
||||
use alloc::format;
|
||||
use alloc::string::String;
|
||||
use alloc::vec::Vec;
|
||||
use codec::{Compact, Decode, Encode};
|
||||
use derive_where::derive_where;
|
||||
|
||||
pub use account_id::AccountId32;
|
||||
pub use account_id20::AccountId20;
|
||||
pub use era::Era;
|
||||
pub use multi_address::MultiAddress;
|
||||
pub use multi_signature::MultiSignature;
|
||||
pub use primitive_types::{H160, H256, H512};
|
||||
pub use static_type::Static;
|
||||
pub use unchecked_extrinsic::UncheckedExtrinsic;
|
||||
pub use wrapper_opaque::WrapperKeepOpaque;
|
||||
pub use yesnomaybe::{Maybe, No, NoMaybe, Yes, YesMaybe, YesNo};
|
||||
|
||||
/// Wraps an already encoded byte vector, prevents being encoded as a raw byte vector as part of
|
||||
/// the transaction payload
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
|
||||
pub struct Encoded(pub Vec<u8>);
|
||||
|
||||
impl codec::Encode for Encoded {
|
||||
fn encode(&self) -> Vec<u8> {
|
||||
self.0.to_owned()
|
||||
}
|
||||
}
|
||||
|
||||
/// Decodes a compact encoded value from the beginning of the provided bytes,
|
||||
/// returning the value and any remaining bytes.
|
||||
pub fn strip_compact_prefix(bytes: &[u8]) -> Result<(u64, &[u8]), codec::Error> {
|
||||
let cursor = &mut &*bytes;
|
||||
let val = <Compact<u64>>::decode(cursor)?;
|
||||
Ok((val.0, *cursor))
|
||||
}
|
||||
|
||||
/// A version of [`core::marker::PhantomData`] that is also Send and Sync (which is fine
|
||||
/// because regardless of the generic param, it is always possible to Send + Sync this
|
||||
/// 0 size type).
|
||||
#[derive(Encode, Decode, scale_info::TypeInfo)]
|
||||
#[derive_where(Clone, PartialEq, Debug, Eq, Default, Hash)]
|
||||
#[scale_info(skip_type_params(T))]
|
||||
#[doc(hidden)]
|
||||
pub struct PhantomDataSendSync<T>(core::marker::PhantomData<T>);
|
||||
|
||||
impl<T> PhantomDataSendSync<T> {
|
||||
pub fn new() -> Self {
|
||||
Self(core::marker::PhantomData)
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl<T> Send for PhantomDataSendSync<T> {}
|
||||
unsafe impl<T> Sync for PhantomDataSendSync<T> {}
|
||||
|
||||
/// This represents a key-value collection and is SCALE compatible
|
||||
/// with collections like BTreeMap. This has the same type params
|
||||
/// as `BTreeMap` which allows us to easily swap the two during codegen.
|
||||
pub type KeyedVec<K, V> = Vec<(K, V)>;
|
||||
|
||||
/// A quick helper to encode some bytes to hex.
|
||||
pub fn to_hex(bytes: impl AsRef<[u8]>) -> String {
|
||||
format!("0x{}", hex::encode(bytes.as_ref()))
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// Copyright 2019-2024 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! The "default" Substrate/Polkadot Address type. This is used in codegen, as well as signing related bits.
|
||||
//! This doesn't contain much functionality itself, but is easy to convert to/from an `sp_runtime::MultiAddress`
|
||||
//! for instance, to gain functionality without forcing a dependency on Substrate crates here.
|
||||
|
||||
use alloc::vec::Vec;
|
||||
use codec::{Decode, Encode};
|
||||
|
||||
/// A multi-format address wrapper for on-chain accounts. This is a simplified version of Substrate's
|
||||
/// `sp_runtime::MultiAddress`.
|
||||
#[derive(
|
||||
Clone,
|
||||
Eq,
|
||||
PartialEq,
|
||||
Ord,
|
||||
PartialOrd,
|
||||
Encode,
|
||||
Decode,
|
||||
Debug,
|
||||
scale_encode::EncodeAsType,
|
||||
scale_decode::DecodeAsType,
|
||||
scale_info::TypeInfo,
|
||||
)]
|
||||
pub enum MultiAddress<AccountId, AccountIndex> {
|
||||
/// It's an account ID (pubkey).
|
||||
Id(AccountId),
|
||||
/// It's an account index.
|
||||
Index(#[codec(compact)] AccountIndex),
|
||||
/// It's some arbitrary raw bytes.
|
||||
Raw(Vec<u8>),
|
||||
/// It's a 32 byte representation.
|
||||
Address32([u8; 32]),
|
||||
/// Its a 20 byte representation.
|
||||
Address20([u8; 20]),
|
||||
}
|
||||
|
||||
impl<AccountId, AccountIndex> From<AccountId> for MultiAddress<AccountId, AccountIndex> {
|
||||
fn from(a: AccountId) -> Self {
|
||||
Self::Id(a)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// Copyright 2019-2024 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! The "default" Substrate/Polkadot Signature type. This is used in codegen, as well as signing related bits.
|
||||
//! This doesn't contain much functionality itself, but is easy to convert to/from an `sp_runtime::MultiSignature`
|
||||
//! for instance, to gain functionality without forcing a dependency on Substrate crates here.
|
||||
|
||||
use codec::{Decode, Encode};
|
||||
|
||||
/// Signature container that can store known signature types. This is a simplified version of
|
||||
/// `sp_runtime::MultiSignature`. To obtain more functionality, convert this into that type.
|
||||
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Encode, Decode, Debug, scale_info::TypeInfo)]
|
||||
pub enum MultiSignature {
|
||||
/// An Ed25519 signature.
|
||||
Ed25519([u8; 64]),
|
||||
/// An Sr25519 signature.
|
||||
Sr25519([u8; 64]),
|
||||
/// An ECDSA/SECP256k1 signature (a 512-bit value, plus 8 bits for recovery ID).
|
||||
Ecdsa([u8; 65]),
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
// Copyright 2019-2024 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
use codec::{Decode, Encode};
|
||||
use scale_decode::{IntoVisitor, TypeResolver, Visitor, visitor::DecodeAsTypeResult};
|
||||
use scale_encode::EncodeAsType;
|
||||
|
||||
use alloc::vec::Vec;
|
||||
|
||||
/// If the type inside this implements [`Encode`], this will implement [`scale_encode::EncodeAsType`].
|
||||
/// If the type inside this implements [`Decode`], this will implement [`scale_decode::DecodeAsType`].
|
||||
///
|
||||
/// In either direction, we ignore any type information and just attempt to encode/decode statically
|
||||
/// via the [`Encode`] and [`Decode`] implementations. This can be useful as an adapter for types which
|
||||
/// do not implement [`scale_encode::EncodeAsType`] and [`scale_decode::DecodeAsType`] themselves, but
|
||||
/// it's best to avoid using it where possible as it will not take into account any type information,
|
||||
/// and is thus more likely to encode or decode incorrectly.
|
||||
#[derive(Debug, Encode, Decode, PartialEq, Eq, Clone, PartialOrd, Ord, Hash)]
|
||||
pub struct Static<T>(pub T);
|
||||
|
||||
impl<T: Encode> EncodeAsType for Static<T> {
|
||||
fn encode_as_type_to<R: TypeResolver>(
|
||||
&self,
|
||||
_type_id: R::TypeId,
|
||||
_types: &R,
|
||||
out: &mut Vec<u8>,
|
||||
) -> Result<(), scale_encode::Error> {
|
||||
self.0.encode_to(out);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct StaticDecodeAsTypeVisitor<T, R>(core::marker::PhantomData<(T, R)>);
|
||||
|
||||
impl<T: Decode, R: TypeResolver> Visitor for StaticDecodeAsTypeVisitor<T, R> {
|
||||
type Value<'scale, 'info> = Static<T>;
|
||||
type Error = scale_decode::Error;
|
||||
type TypeResolver = R;
|
||||
|
||||
fn unchecked_decode_as_type<'scale, 'info>(
|
||||
self,
|
||||
input: &mut &'scale [u8],
|
||||
_type_id: R::TypeId,
|
||||
_types: &'info R,
|
||||
) -> DecodeAsTypeResult<Self, Result<Self::Value<'scale, 'info>, Self::Error>> {
|
||||
use scale_decode::{Error, visitor::DecodeError};
|
||||
let decoded = T::decode(input)
|
||||
.map(Static)
|
||||
.map_err(|e| Error::new(DecodeError::CodecError(e).into()));
|
||||
DecodeAsTypeResult::Decoded(decoded)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Decode> IntoVisitor for Static<T> {
|
||||
type AnyVisitor<R: TypeResolver> = StaticDecodeAsTypeVisitor<T, R>;
|
||||
fn into_visitor<R: TypeResolver>() -> StaticDecodeAsTypeVisitor<T, R> {
|
||||
StaticDecodeAsTypeVisitor(core::marker::PhantomData)
|
||||
}
|
||||
}
|
||||
|
||||
// Make it easy to convert types into Static where required.
|
||||
impl<T> From<T> for Static<T> {
|
||||
fn from(value: T) -> Self {
|
||||
Static(value)
|
||||
}
|
||||
}
|
||||
|
||||
// Static<T> is just a marker type and should be as transparent as possible:
|
||||
impl<T> core::ops::Deref for Static<T> {
|
||||
type Target = T;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> core::ops::DerefMut for Static<T> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
// Copyright 2019-2024 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! The "default" Substrate/Polkadot UncheckedExtrinsic.
|
||||
//! This is used in codegen for runtime API calls.
|
||||
//!
|
||||
//! The inner bytes represent the encoded extrinsic expected by the
|
||||
//! runtime APIs. Deriving `EncodeAsType` would lead to the inner
|
||||
//! bytes to be re-encoded (length prefixed).
|
||||
|
||||
use core::marker::PhantomData;
|
||||
|
||||
use codec::{Decode, Encode};
|
||||
use scale_decode::{DecodeAsType, IntoVisitor, TypeResolver, Visitor, visitor::DecodeAsTypeResult};
|
||||
|
||||
use super::{Encoded, Static};
|
||||
use alloc::vec::Vec;
|
||||
|
||||
/// The unchecked extrinsic from substrate.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Encode)]
|
||||
pub struct UncheckedExtrinsic<Address, Call, Signature, Extra>(
|
||||
Static<Encoded>,
|
||||
#[codec(skip)] PhantomData<(Address, Call, Signature, Extra)>,
|
||||
);
|
||||
|
||||
impl<Address, Call, Signature, Extra> UncheckedExtrinsic<Address, Call, Signature, Extra> {
|
||||
/// Construct a new [`UncheckedExtrinsic`].
|
||||
pub fn new(bytes: Vec<u8>) -> Self {
|
||||
Self(Static(Encoded(bytes)), PhantomData)
|
||||
}
|
||||
|
||||
/// Get the bytes of the encoded extrinsic.
|
||||
pub fn bytes(&self) -> &[u8] {
|
||||
self.0.0.0.as_slice()
|
||||
}
|
||||
}
|
||||
|
||||
impl<Address, Call, Signature, Extra> Decode
|
||||
for UncheckedExtrinsic<Address, Call, Signature, Extra>
|
||||
{
|
||||
fn decode<I: codec::Input>(input: &mut I) -> Result<Self, codec::Error> {
|
||||
// The bytes for an UncheckedExtrinsic are first a compact
|
||||
// encoded length, and then the bytes following. This is the
|
||||
// same encoding as a Vec, so easiest ATM is just to decode
|
||||
// into that, and then encode the vec bytes to get our extrinsic
|
||||
// bytes, which we save into an `Encoded` to preserve as-is.
|
||||
let xt_vec: Vec<u8> = Decode::decode(input)?;
|
||||
Ok(UncheckedExtrinsic::new(xt_vec))
|
||||
}
|
||||
}
|
||||
|
||||
impl<Address, Call, Signature, Extra> scale_encode::EncodeAsType
|
||||
for UncheckedExtrinsic<Address, Call, Signature, Extra>
|
||||
{
|
||||
fn encode_as_type_to<R: TypeResolver>(
|
||||
&self,
|
||||
type_id: R::TypeId,
|
||||
types: &R,
|
||||
out: &mut Vec<u8>,
|
||||
) -> Result<(), scale_encode::Error> {
|
||||
self.0.encode_as_type_to(type_id, types, out)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Address, Call, Signature, Extra> From<Vec<u8>>
|
||||
for UncheckedExtrinsic<Address, Call, Signature, Extra>
|
||||
{
|
||||
fn from(bytes: Vec<u8>) -> Self {
|
||||
UncheckedExtrinsic::new(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Address, Call, Signature, Extra> From<UncheckedExtrinsic<Address, Call, Signature, Extra>>
|
||||
for Vec<u8>
|
||||
{
|
||||
fn from(bytes: UncheckedExtrinsic<Address, Call, Signature, Extra>) -> Self {
|
||||
bytes.0.0.0
|
||||
}
|
||||
}
|
||||
|
||||
pub struct UncheckedExtrinsicDecodeAsTypeVisitor<Address, Call, Signature, Extra, R: TypeResolver>(
|
||||
PhantomData<(Address, Call, Signature, Extra, R)>,
|
||||
);
|
||||
|
||||
impl<Address, Call, Signature, Extra, R: TypeResolver> Visitor
|
||||
for UncheckedExtrinsicDecodeAsTypeVisitor<Address, Call, Signature, Extra, R>
|
||||
{
|
||||
type Value<'scale, 'info> = UncheckedExtrinsic<Address, Call, Signature, Extra>;
|
||||
type Error = scale_decode::Error;
|
||||
type TypeResolver = R;
|
||||
|
||||
fn unchecked_decode_as_type<'scale, 'info>(
|
||||
self,
|
||||
input: &mut &'scale [u8],
|
||||
type_id: R::TypeId,
|
||||
types: &'info R,
|
||||
) -> DecodeAsTypeResult<Self, Result<Self::Value<'scale, 'info>, Self::Error>> {
|
||||
DecodeAsTypeResult::Decoded(Self::Value::decode_as_type(input, type_id, types))
|
||||
}
|
||||
}
|
||||
|
||||
impl<Address, Call, Signature, Extra> IntoVisitor
|
||||
for UncheckedExtrinsic<Address, Call, Signature, Extra>
|
||||
{
|
||||
type AnyVisitor<R: TypeResolver> =
|
||||
UncheckedExtrinsicDecodeAsTypeVisitor<Address, Call, Signature, Extra, R>;
|
||||
|
||||
fn into_visitor<R: TypeResolver>()
|
||||
-> UncheckedExtrinsicDecodeAsTypeVisitor<Address, Call, Signature, Extra, R> {
|
||||
UncheckedExtrinsicDecodeAsTypeVisitor(PhantomData)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use super::*;
|
||||
|
||||
use alloc::vec;
|
||||
|
||||
#[test]
|
||||
fn unchecked_extrinsic_encoding() {
|
||||
// A tx is basically some bytes with a compact length prefix; ie an encoded vec:
|
||||
let tx_bytes = vec![1u8, 2, 3].encode();
|
||||
|
||||
let unchecked_extrinsic = UncheckedExtrinsic::<(), (), (), ()>::new(tx_bytes.clone());
|
||||
let encoded_tx_bytes = unchecked_extrinsic.encode();
|
||||
|
||||
// The encoded representation must not alter the provided bytes.
|
||||
assert_eq!(tx_bytes, encoded_tx_bytes);
|
||||
|
||||
// However, for decoding we expect to be able to read the extrinsic from the wire
|
||||
// which would be length prefixed.
|
||||
let decoded_tx = UncheckedExtrinsic::<(), (), (), ()>::decode(&mut &tx_bytes[..]).unwrap();
|
||||
let decoded_tx_bytes = decoded_tx.bytes();
|
||||
let encoded_tx_bytes = decoded_tx.encode();
|
||||
|
||||
assert_eq!(decoded_tx_bytes, encoded_tx_bytes);
|
||||
// Ensure we can decode the tx and fetch only the tx bytes.
|
||||
assert_eq!(vec![1, 2, 3], encoded_tx_bytes);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
// Copyright 2019-2024 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
use super::PhantomDataSendSync;
|
||||
use codec::{Compact, Decode, DecodeAll, Encode};
|
||||
use derive_where::derive_where;
|
||||
use scale_decode::{IntoVisitor, TypeResolver, Visitor, ext::scale_type_resolver::visitor};
|
||||
use scale_encode::EncodeAsType;
|
||||
|
||||
use alloc::format;
|
||||
use alloc::vec::Vec;
|
||||
|
||||
/// A wrapper for any type `T` which implement encode/decode in a way compatible with `Vec<u8>`.
|
||||
/// [`WrapperKeepOpaque`] stores the type only in its opaque format, aka as a `Vec<u8>`. To
|
||||
/// access the real type `T` [`Self::try_decode`] needs to be used.
|
||||
// Dev notes:
|
||||
//
|
||||
// - This is adapted from [here](https://github.com/paritytech/substrate/blob/master/frame/support/src/traits/misc.rs).
|
||||
// - The encoded bytes will be a compact encoded length followed by that number of bytes.
|
||||
// - However, the TypeInfo describes the type as a composite with first a compact encoded length and next the type itself.
|
||||
// [`Encode`] and [`Decode`] impls will "just work" to take this into a `Vec<u8>`, but we need a custom [`EncodeAsType`]
|
||||
// and [`Visitor`] implementation to encode and decode based on TypeInfo.
|
||||
#[derive(Encode, Decode)]
|
||||
#[derive_where(Debug, Clone, PartialEq, Eq, Default, Hash)]
|
||||
pub struct WrapperKeepOpaque<T> {
|
||||
data: Vec<u8>,
|
||||
_phantom: PhantomDataSendSync<T>,
|
||||
}
|
||||
|
||||
impl<T> WrapperKeepOpaque<T> {
|
||||
/// Try to decode the wrapped type from the inner `data`.
|
||||
///
|
||||
/// Returns `None` if the decoding failed.
|
||||
pub fn try_decode(&self) -> Option<T>
|
||||
where
|
||||
T: Decode,
|
||||
{
|
||||
T::decode_all(&mut &self.data[..]).ok()
|
||||
}
|
||||
|
||||
/// Returns the length of the encoded `T`.
|
||||
pub fn encoded_len(&self) -> usize {
|
||||
self.data.len()
|
||||
}
|
||||
|
||||
/// Returns the encoded data.
|
||||
pub fn encoded(&self) -> &[u8] {
|
||||
&self.data
|
||||
}
|
||||
|
||||
/// Create from the given encoded `data`.
|
||||
pub fn from_encoded(data: Vec<u8>) -> Self {
|
||||
Self {
|
||||
data,
|
||||
_phantom: PhantomDataSendSync::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create from some raw value by encoding it.
|
||||
pub fn from_value(value: T) -> Self
|
||||
where
|
||||
T: Encode,
|
||||
{
|
||||
Self {
|
||||
data: value.encode(),
|
||||
_phantom: PhantomDataSendSync::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> EncodeAsType for WrapperKeepOpaque<T> {
|
||||
fn encode_as_type_to<R: TypeResolver>(
|
||||
&self,
|
||||
type_id: R::TypeId,
|
||||
types: &R,
|
||||
out: &mut Vec<u8>,
|
||||
) -> Result<(), scale_encode::Error> {
|
||||
use scale_encode::error::{Error, ErrorKind, Kind};
|
||||
|
||||
let ctx = (type_id.clone(), out);
|
||||
let visitor = visitor::new(ctx, |(type_id, _out), _| {
|
||||
// Check that the target shape lines up: any other shape but composite is wrong.
|
||||
Err(Error::new(ErrorKind::WrongShape {
|
||||
actual: Kind::Struct,
|
||||
expected_id: format!("{type_id:?}"),
|
||||
}))
|
||||
})
|
||||
.visit_composite(|(_type_id, out), _path, _fields| {
|
||||
self.data.encode_to(out);
|
||||
Ok(())
|
||||
});
|
||||
|
||||
types
|
||||
.resolve_type(type_id.clone(), visitor)
|
||||
.map_err(|_| Error::new(ErrorKind::TypeNotFound(format!("{type_id:?}"))))?
|
||||
}
|
||||
}
|
||||
|
||||
pub struct WrapperKeepOpaqueVisitor<T, R>(core::marker::PhantomData<(T, R)>);
|
||||
impl<T, R: TypeResolver> Visitor for WrapperKeepOpaqueVisitor<T, R> {
|
||||
type Value<'scale, 'info> = WrapperKeepOpaque<T>;
|
||||
type Error = scale_decode::Error;
|
||||
type TypeResolver = R;
|
||||
|
||||
fn visit_composite<'scale, 'info>(
|
||||
self,
|
||||
value: &mut scale_decode::visitor::types::Composite<'scale, 'info, R>,
|
||||
_type_id: R::TypeId,
|
||||
) -> Result<Self::Value<'scale, 'info>, Self::Error> {
|
||||
use scale_decode::error::{Error, ErrorKind};
|
||||
use scale_decode::visitor::DecodeError;
|
||||
|
||||
if value.name() != Some("WrapperKeepOpaque") {
|
||||
return Err(Error::new(ErrorKind::VisitorDecodeError(
|
||||
DecodeError::TypeResolvingError(format!(
|
||||
"Expected a type named 'WrapperKeepOpaque', got: {:?}",
|
||||
value.name()
|
||||
)),
|
||||
)));
|
||||
}
|
||||
|
||||
if value.remaining() != 2 {
|
||||
return Err(Error::new(ErrorKind::WrongLength {
|
||||
actual_len: value.remaining(),
|
||||
expected_len: 2,
|
||||
}));
|
||||
}
|
||||
|
||||
// The field to decode is a compact len followed by bytes. Decode the length, then grab the bytes.
|
||||
let Compact(len) = value
|
||||
.decode_item(Compact::<u32>::into_visitor())
|
||||
.expect("length checked")?;
|
||||
let field = value.next().expect("length checked")?;
|
||||
|
||||
// Sanity check that the compact length we decoded lines up with the number of bytes encoded in the next field.
|
||||
if field.bytes().len() != len as usize {
|
||||
return Err(Error::custom_str(
|
||||
"WrapperTypeKeepOpaque compact encoded length doesn't line up with encoded byte len",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(WrapperKeepOpaque {
|
||||
data: field.bytes().to_vec(),
|
||||
_phantom: PhantomDataSendSync::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> IntoVisitor for WrapperKeepOpaque<T> {
|
||||
type AnyVisitor<R: TypeResolver> = WrapperKeepOpaqueVisitor<T, R>;
|
||||
fn into_visitor<R: TypeResolver>() -> WrapperKeepOpaqueVisitor<T, R> {
|
||||
WrapperKeepOpaqueVisitor(core::marker::PhantomData)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use scale_decode::DecodeAsType;
|
||||
|
||||
use alloc::vec;
|
||||
|
||||
use super::*;
|
||||
|
||||
// Copied from https://github.com/paritytech/substrate/blob/master/frame/support/src/traits/misc.rs
|
||||
// and used for tests to check that we can work with the expected TypeInfo without needing to import
|
||||
// the frame_support crate, which has quite a lot of dependencies.
|
||||
impl<T: scale_info::TypeInfo + 'static> scale_info::TypeInfo for WrapperKeepOpaque<T> {
|
||||
type Identity = Self;
|
||||
fn type_info() -> scale_info::Type {
|
||||
use scale_info::{Path, Type, TypeParameter, build::Fields, meta_type};
|
||||
|
||||
Type::builder()
|
||||
.path(Path::new("WrapperKeepOpaque", module_path!()))
|
||||
.type_params(vec![TypeParameter::new("T", Some(meta_type::<T>()))])
|
||||
.composite(
|
||||
Fields::unnamed()
|
||||
.field(|f| f.compact::<u32>())
|
||||
.field(|f| f.ty::<T>().type_name("T")),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Given a type definition, return type ID and registry representing it.
|
||||
fn make_type<T: scale_info::TypeInfo + 'static>() -> (u32, scale_info::PortableRegistry) {
|
||||
let m = scale_info::MetaType::new::<T>();
|
||||
let mut types = scale_info::Registry::new();
|
||||
let id = types.register_type(&m);
|
||||
let portable_registry: scale_info::PortableRegistry = types.into();
|
||||
(id.id, portable_registry)
|
||||
}
|
||||
|
||||
fn roundtrips_like_scale_codec<T>(t: T)
|
||||
where
|
||||
T: EncodeAsType
|
||||
+ DecodeAsType
|
||||
+ Encode
|
||||
+ Decode
|
||||
+ PartialEq
|
||||
+ core::fmt::Debug
|
||||
+ scale_info::TypeInfo
|
||||
+ 'static,
|
||||
{
|
||||
let (type_id, types) = make_type::<T>();
|
||||
|
||||
let scale_codec_encoded = t.encode();
|
||||
let encode_as_type_encoded = t.encode_as_type(type_id, &types).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
scale_codec_encoded, encode_as_type_encoded,
|
||||
"encoded bytes should match"
|
||||
);
|
||||
|
||||
let decode_as_type_bytes = &mut &*scale_codec_encoded;
|
||||
let decoded_as_type = T::decode_as_type(decode_as_type_bytes, type_id, &types)
|
||||
.expect("decode-as-type decodes");
|
||||
|
||||
let decode_scale_codec_bytes = &mut &*scale_codec_encoded;
|
||||
let decoded_scale_codec = T::decode(decode_scale_codec_bytes).expect("scale-codec decodes");
|
||||
|
||||
assert!(
|
||||
decode_as_type_bytes.is_empty(),
|
||||
"no bytes should remain in decode-as-type impl"
|
||||
);
|
||||
assert!(
|
||||
decode_scale_codec_bytes.is_empty(),
|
||||
"no bytes should remain in codec-decode impl"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
decoded_as_type, decoded_scale_codec,
|
||||
"decoded values should match"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wrapper_keep_opaque_roundtrips_ok() {
|
||||
roundtrips_like_scale_codec(WrapperKeepOpaque::from_value(123u64));
|
||||
roundtrips_like_scale_codec(WrapperKeepOpaque::from_value(true));
|
||||
roundtrips_like_scale_codec(WrapperKeepOpaque::from_value(vec![1u8, 2, 3, 4]));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
// Copyright 2019-2025 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
/// A unit marker enum.
|
||||
pub enum Yes {}
|
||||
/// A unit marker enum.
|
||||
pub enum Maybe {}
|
||||
/// A unit marker enum.
|
||||
pub enum No {}
|
||||
|
||||
/// This is implemented for [`Yes`] and [`No`] and
|
||||
/// allows us to check at runtime which of these types is present.
|
||||
pub trait YesNo {
|
||||
/// [`Yes`]
|
||||
fn is_yes() -> bool {
|
||||
false
|
||||
}
|
||||
/// [`No`]
|
||||
fn is_no() -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl YesNo for Yes {
|
||||
fn is_yes() -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
impl YesNo for No {
|
||||
fn is_no() -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/// This is implemented for [`Yes`] and [`Maybe`] and
|
||||
/// allows us to check at runtime which of these types is present.
|
||||
pub trait YesMaybe {
|
||||
/// [`Yes`]
|
||||
fn is_yes() -> bool {
|
||||
false
|
||||
}
|
||||
/// [`Maybe`]
|
||||
fn is_maybe() -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl YesMaybe for Yes {
|
||||
fn is_yes() -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
impl YesMaybe for Maybe {
|
||||
fn is_maybe() -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/// This is implemented for [`No`] and [`Maybe`] and
|
||||
/// allows us to check at runtime which of these types is present.
|
||||
pub trait NoMaybe {
|
||||
/// [`No`]
|
||||
fn is_no() -> bool {
|
||||
false
|
||||
}
|
||||
/// [`Maybe`]
|
||||
fn is_maybe() -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl NoMaybe for No {
|
||||
fn is_no() -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
impl NoMaybe for Maybe {
|
||||
fn is_maybe() -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
// Copyright 2019-2024 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! Encode View Function payloads, decode the associated values returned from them, and validate
|
||||
//! static View Function payloads.
|
||||
|
||||
pub mod payload;
|
||||
|
||||
use crate::Metadata;
|
||||
use crate::error::ViewFunctionError;
|
||||
use alloc::string::ToString;
|
||||
use alloc::vec::Vec;
|
||||
use payload::Payload;
|
||||
use scale_decode::IntoVisitor;
|
||||
|
||||
/// Run the validation logic against some View Function payload you'd like to use. Returns `Ok(())`
|
||||
/// if the payload is valid (or if it's not possible to check since the payload has no validation hash).
|
||||
/// Return an error if the payload was not valid or something went wrong trying to validate it (ie
|
||||
/// the View Function in question do not exist at all)
|
||||
pub fn validate<P: Payload>(payload: P, metadata: &Metadata) -> Result<(), ViewFunctionError> {
|
||||
let Some(hash) = payload.validation_hash() else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let pallet_name = payload.pallet_name();
|
||||
let function_name = payload.function_name();
|
||||
|
||||
let view_function = metadata
|
||||
.pallet_by_name(pallet_name)
|
||||
.ok_or_else(|| ViewFunctionError::PalletNotFound(pallet_name.to_string()))?
|
||||
.view_function_by_name(function_name)
|
||||
.ok_or_else(|| ViewFunctionError::ViewFunctionNotFound {
|
||||
pallet_name: pallet_name.to_string(),
|
||||
function_name: function_name.to_string(),
|
||||
})?;
|
||||
|
||||
if hash != view_function.hash() {
|
||||
Err(ViewFunctionError::IncompatibleCodegen)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// The name of the Runtime API call which can execute
|
||||
pub const CALL_NAME: &str = "RuntimeViewFunction_execute_view_function";
|
||||
|
||||
/// Encode the bytes that will be passed to the "execute_view_function" Runtime API call,
|
||||
/// to execute the View Function represented by the given payload.
|
||||
pub fn call_args<P: Payload>(
|
||||
payload: P,
|
||||
metadata: &Metadata,
|
||||
) -> Result<Vec<u8>, ViewFunctionError> {
|
||||
let inputs = frame_decode::view_functions::encode_view_function_inputs(
|
||||
payload.pallet_name(),
|
||||
payload.function_name(),
|
||||
payload.args(),
|
||||
metadata,
|
||||
metadata.types(),
|
||||
)
|
||||
.map_err(ViewFunctionError::CouldNotEncodeInputs)?;
|
||||
|
||||
Ok(inputs)
|
||||
}
|
||||
|
||||
/// Decode the value bytes at the location given by the provided View Function payload.
|
||||
pub fn decode_value<P: Payload>(
|
||||
bytes: &mut &[u8],
|
||||
payload: P,
|
||||
metadata: &Metadata,
|
||||
) -> Result<P::ReturnType, ViewFunctionError> {
|
||||
let value = frame_decode::view_functions::decode_view_function_response(
|
||||
payload.pallet_name(),
|
||||
payload.function_name(),
|
||||
bytes,
|
||||
metadata,
|
||||
metadata.types(),
|
||||
P::ReturnType::into_visitor(),
|
||||
)
|
||||
.map_err(ViewFunctionError::CouldNotDecodeResponse)?;
|
||||
|
||||
Ok(value)
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
// Copyright 2019-2024 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! This module contains the trait and types used to represent
|
||||
//! View Function calls that can be made.
|
||||
|
||||
use alloc::borrow::Cow;
|
||||
use alloc::string::String;
|
||||
use core::marker::PhantomData;
|
||||
use derive_where::derive_where;
|
||||
use frame_decode::view_functions::IntoEncodableValues;
|
||||
use scale_decode::DecodeAsType;
|
||||
|
||||
/// This represents a View Function payload that can call into the runtime of node.
|
||||
///
|
||||
/// # Components
|
||||
///
|
||||
/// - associated return type
|
||||
///
|
||||
/// Resulting bytes of the call are interpreted into this type.
|
||||
///
|
||||
/// - query ID
|
||||
///
|
||||
/// The ID used to identify in the runtime which view function to call.
|
||||
///
|
||||
/// - encoded arguments
|
||||
///
|
||||
/// Each argument of the View Function must be scale-encoded.
|
||||
pub trait Payload {
|
||||
/// Type of the arguments for this call.
|
||||
type ArgsType: IntoEncodableValues;
|
||||
/// The return type of the function call.
|
||||
type ReturnType: DecodeAsType;
|
||||
|
||||
/// The View Function pallet name.
|
||||
fn pallet_name(&self) -> &str;
|
||||
|
||||
/// The View Function function name.
|
||||
fn function_name(&self) -> &str;
|
||||
|
||||
/// The arguments.
|
||||
fn args(&self) -> &Self::ArgsType;
|
||||
|
||||
/// Returns the statically generated validation hash.
|
||||
fn validation_hash(&self) -> Option<[u8; 32]> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
// A reference to a payload is a valid payload.
|
||||
impl<P: Payload + ?Sized> Payload for &'_ P {
|
||||
type ArgsType = P::ArgsType;
|
||||
type ReturnType = P::ReturnType;
|
||||
|
||||
fn pallet_name(&self) -> &str {
|
||||
P::pallet_name(*self)
|
||||
}
|
||||
|
||||
fn function_name(&self) -> &str {
|
||||
P::function_name(*self)
|
||||
}
|
||||
|
||||
fn args(&self) -> &Self::ArgsType {
|
||||
P::args(*self)
|
||||
}
|
||||
|
||||
fn validation_hash(&self) -> Option<[u8; 32]> {
|
||||
P::validation_hash(*self)
|
||||
}
|
||||
}
|
||||
|
||||
/// A View Function payload containing the generic argument data
|
||||
/// and interpreting the result of the call as `ReturnType`.
|
||||
///
|
||||
/// This can be created from static values (ie those generated
|
||||
/// via the `subxt` macro) or dynamic values via [`dynamic`].
|
||||
#[derive_where(Clone, Debug, Eq, Ord, PartialEq, PartialOrd; ArgsType)]
|
||||
pub struct StaticPayload<ArgsType, ReturnType> {
|
||||
pallet_name: Cow<'static, str>,
|
||||
function_name: Cow<'static, str>,
|
||||
args: ArgsType,
|
||||
validation_hash: Option<[u8; 32]>,
|
||||
_marker: PhantomData<ReturnType>,
|
||||
}
|
||||
|
||||
/// A dynamic View Function payload.
|
||||
pub type DynamicPayload<ArgsType, ReturnType> = StaticPayload<ArgsType, ReturnType>;
|
||||
|
||||
impl<ArgsType: IntoEncodableValues, ReturnType: DecodeAsType> Payload
|
||||
for StaticPayload<ArgsType, ReturnType>
|
||||
{
|
||||
type ArgsType = ArgsType;
|
||||
type ReturnType = ReturnType;
|
||||
|
||||
fn pallet_name(&self) -> &str {
|
||||
&self.pallet_name
|
||||
}
|
||||
|
||||
fn function_name(&self) -> &str {
|
||||
&self.function_name
|
||||
}
|
||||
|
||||
fn args(&self) -> &Self::ArgsType {
|
||||
&self.args
|
||||
}
|
||||
|
||||
fn validation_hash(&self) -> Option<[u8; 32]> {
|
||||
self.validation_hash
|
||||
}
|
||||
}
|
||||
|
||||
impl<ReturnTy, ArgsType> StaticPayload<ArgsType, ReturnTy> {
|
||||
/// Create a new [`StaticPayload`] for a View Function call.
|
||||
pub fn new(
|
||||
pallet_name: impl Into<String>,
|
||||
function_name: impl Into<String>,
|
||||
args: ArgsType,
|
||||
) -> Self {
|
||||
StaticPayload {
|
||||
pallet_name: pallet_name.into().into(),
|
||||
function_name: function_name.into().into(),
|
||||
args,
|
||||
validation_hash: None,
|
||||
_marker: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new static [`StaticPayload`] for a View Function call
|
||||
/// using static function name and scale-encoded argument data.
|
||||
///
|
||||
/// This is only expected to be used from codegen.
|
||||
#[doc(hidden)]
|
||||
pub fn new_static(
|
||||
pallet_name: &'static str,
|
||||
function_name: &'static str,
|
||||
args: ArgsType,
|
||||
hash: [u8; 32],
|
||||
) -> StaticPayload<ArgsType, ReturnTy> {
|
||||
StaticPayload {
|
||||
pallet_name: Cow::Borrowed(pallet_name),
|
||||
function_name: Cow::Borrowed(function_name),
|
||||
args,
|
||||
validation_hash: Some(hash),
|
||||
_marker: core::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Do not validate this call prior to submitting it.
|
||||
pub fn unvalidated(self) -> Self {
|
||||
Self {
|
||||
validation_hash: None,
|
||||
..self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new [`DynamicPayload`] to call a View Function.
|
||||
pub fn dynamic<ArgsType, ReturnType>(
|
||||
pallet_name: impl Into<String>,
|
||||
function_name: impl Into<String>,
|
||||
args: ArgsType,
|
||||
) -> DynamicPayload<ArgsType, ReturnType> {
|
||||
DynamicPayload::new(pallet_name, function_name, args)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user