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